{"owner":"RunanywhereAI","repo":"runanywhere-sdks","hasSkills":true,"totalSkillsCount":91,"totalTokensCount":114688,"categories":["anthropic-skill","root-instruction","claude-rule","command-prompt","plugin-manifest"],"hasMcp":false,"mcpConfig":null,"found":[".agents/skills/telemetry-e2e/SKILL.md",".agents/skills/sdk-test-starters/SKILL.md",".agents/skills/engine-plugin-authoring/SKILL.md",".agents/skills/sdk-release/SKILL.md",".claude/skills/engine-plugin-authoring/SKILL.md",".agents/skills/sdk-publish/SKILL.md",".claude/skills/sdk-release/SKILL.md","AGENTS.md",".claude/skills/sdk-test-starters/SKILL.md","CLAUDE.md",".claude/skills/telemetry-e2e/SKILL.md",".claude/skills/sdk-publish/SKILL.md","core/AGENTS.md","core/CLAUDE.md","engines/AGENTS.md","engines/CLAUDE.md","idl/AGENTS.md","idl/CLAUDE.md","runtimes/AGENTS.md","runtimes/CLAUDE.md","scripts/AGENTS.md","scripts/CLAUDE.md",".claude/commands/address_PR_comments.md",".claude/commands/copy_over_logic.md",".claude/commands/run_and_fix_lints.md","bindings/electron/packages/llamacpp/README.md","bindings/electron/packages/llamacpp/package-lock.json","bindings/electron/packages/llamacpp/package.json","bindings/electron/packages/llamacpp/tsconfig.json","bindings/electron/packages/neurt/README.md","bindings/electron/packages/neurt/package-lock.json","bindings/electron/packages/neurt/package.json","bindings/electron/packages/neurt/tsconfig.json","bindings/electron/packages/onnx/README.md","bindings/electron/packages/onnx/package-lock.json","bindings/electron/packages/onnx/package.json","bindings/electron/packages/onnx/tsconfig.json","bindings/electron/packages/qhexrt/README.md","bindings/electron/packages/qhexrt/package-lock.json","bindings/electron/packages/qhexrt/package.json","bindings/electron/packages/qhexrt/tsconfig.json","bindings/electron/packages/sherpa/README.md","bindings/electron/packages/sherpa/package-lock.json","bindings/electron/packages/sherpa/package.json","bindings/electron/packages/sherpa/tsconfig.json","bindings/flutter/packages/runanywhere/CHANGELOG.md","bindings/flutter/packages/runanywhere/README.md","bindings/flutter/packages/runanywhere/pubspec.yaml","bindings/flutter/packages/runanywhere_llamacpp/CHANGELOG.md","bindings/flutter/packages/runanywhere_llamacpp/README.md","bindings/flutter/packages/runanywhere_llamacpp/pubspec.yaml","bindings/flutter/packages/runanywhere_mlx/CHANGELOG.md","bindings/flutter/packages/runanywhere_mlx/README.md","bindings/flutter/packages/runanywhere_mlx/pubspec.yaml","bindings/flutter/packages/runanywhere_onnx/CHANGELOG.md","bindings/flutter/packages/runanywhere_onnx/README.md","bindings/flutter/packages/runanywhere_onnx/pubspec.yaml","bindings/flutter/packages/runanywhere_qhexrt/CHANGELOG.md","bindings/flutter/packages/runanywhere_qhexrt/README.md","bindings/flutter/packages/runanywhere_qhexrt/pubspec.yaml","bindings/react-native/packages/core/README.md","bindings/react-native/packages/core/nitro.json","bindings/react-native/packages/core/package.json","bindings/react-native/packages/core/tsconfig.json","bindings/react-native/packages/core/tsconfig.test.json","bindings/react-native/packages/llamacpp/README.md","bindings/react-native/packages/llamacpp/nitro.json","bindings/react-native/packages/llamacpp/package.json","bindings/react-native/packages/llamacpp/tsconfig.json","bindings/react-native/packages/mlx/README.md","bindings/react-native/packages/mlx/package.json","bindings/react-native/packages/mlx/tsconfig.json","bindings/react-native/packages/onnx/README.md","bindings/react-native/packages/onnx/nitro.json","bindings/react-native/packages/onnx/package.json","bindings/react-native/packages/onnx/tsconfig.json","bindings/react-native/packages/qhexrt/README.md","bindings/react-native/packages/qhexrt/nitro.json","bindings/react-native/packages/qhexrt/package.json","bindings/react-native/packages/qhexrt/tsconfig.json","bindings/react-native/packages/qhexrt/tsconfig.test.json","bindings/web/packages/core/README.md","bindings/web/packages/core/package.json","bindings/web/packages/core/tsconfig.json","bindings/web/packages/core/tsconfig.test.json","bindings/web/packages/llamacpp/README.md","bindings/web/packages/llamacpp/package.json","bindings/web/packages/llamacpp/tsconfig.json","bindings/web/packages/onnx/README.md","bindings/web/packages/onnx/package.json","bindings/web/packages/onnx/tsconfig.json"],"skills":{".agents/skills/telemetry-e2e/SKILL.md":"---\nname: telemetry-e2e\ndescription: Prove that what an SDK binding EMITS on the telemetry wire is correct and that it actually arrives — vocabulary compliance, presence/absence semantics, device-id stability, and a received request body / stored row as evidence. Use when touching telemetry_json.cpp, telemetry_manager.cpp, rac_telemetry_vocabulary.h, any binding's platform-adapter telemetry bridge, or when asked to verify a telemetry field isn't silently dropping.\n---\n\n# Telemetry Wire Correctness (SDK side)\n\nThis is the SDK-side counterpart to the backend's `telemetry-e2e` skill (private\n`Runanywhere-monorepo` repo — proves event-to-stored-row on the server side). This one\nproves the other half: that commons *serializes* a call correctly and that the bytes\n*leaving* a binding are the bytes a real backend accepts. It does not cover per-starter-app\ninference smoke tests — see `sdk-test-starters` for that.\n\n## 0. The evidence bar\n\nCompiling is not evidence. A green `telemetry_extraction_tests` run is not evidence that a\nreal binding's real call site reaches the wire correctly — it only proves the *serializer*\nhandles a hand-built payload correctly. In order of increasing strength:\n\n1. **Unit test green** (`ctest -R telemetry_extraction_tests`) — proves the JSON builder's\n   presence/vocabulary rules are still correct in isolation. Necessary, not sufficient.\n2. **Received request body** — capture the actual JSON `rcli` (or a real binding) put on\n   the wire and read it: right modality endpoint, right keys, no vocabulary values that\n   got through that shouldn't have, measured zeros kept as `0` not `null`.\n3. **Stored row matches, field-for-field** — the backend's own ingest response\n   (`events_received`/`events_stored`/`events_skipped`) or a direct DB read shows the row\n   landed with the values you actually sent, not just a 2xx. This is the only tier that\n   also proves the vocabulary/schema on both sides still agree.\n\nA PR description that says \"sent telemetry, no errors\" without quoting a request body or a\nstored-row count is not evidence of anything except that a socket didn't error out.\n\n## 1. The emit path\n\nEvery binding funnels through the same two C ABI entry points in\n`core/src/infrastructure/telemetry/telemetry_manager.cpp`:\n\n- `rac_telemetry_manager_track` — flat-struct payload (`rac_telemetry_payload_t`), used by\n  `rcli`'s `telemetry emit`/`telemetry blast` and by hand-built events.\n- `rac_telemetry_manager_track_proto` — the real path every language binding actually uses:\n  a binding builds a protobuf `SDKEvent` (`sdk_events.pb.h`) at the real call site (e.g. an\n  LLM `generate()` completing), and `proto_event_type_string()` + the per-domain extraction\n  blocks in `telemetry_manager.cpp` turn it into the same flat payload. `framework_proto_to_string()`\n  / `framework_to_string()` / `clean_framework()` do the backend-name normalization here —\n  this is where a raw enum like `INFERENCE_FRAMEWORK_LLAMA_CPP` becomes the wire string `\"llamacpp\"`.\n- `stamp_live_device_state()` (same file) then overwrites the SDK-origin/device-state fields\n  (`sdk_binding`, `platform`, `battery_state`, memory, core count) from manager-owned state,\n  not from whatever the caller passed — a binding cannot inject a platform or binding value.\n- The payload is handed to `rac_telemetry_manager_payload_to_json()` in\n  `core/src/infrastructure/telemetry/telemetry_json.cpp`, which does field-by-field\n  serialization (see §2/§3 for the two contracts enforced there).\n- `send_batch_json()` batches by modality and POSTs to `/api/v2/sdk/telemetry/{modality}`\n  through whichever `rac_http_transport_ops_t` the binding registered (URLSession/OkHttp/\n  `emscripten_fetch`/curl for `rcli` — see the root `AGENTS.md`'s \"HTTP is platform-provided\"\n  rule). There is no `libcurl` fallback baked into commons itself.\n\nIf a field is going missing on the wire, the fix is almost always in\n`telemetry_json.cpp` (a field never wired into the serializer) or in the call site's proto\npopulation (a field never set on the `SDKEvent`) — not in a binding's bridge code, which\njust forwards bytes.\n\n## 2. The closed vocabularies — and the silent-drop rule\n\n`core/include/rac/infrastructure/telemetry/rac_telemetry_vocabulary.h` is a **generated\nfile** (see §5) holding six closed tables, each with a `RAC_TELEMETRY_<NAME>_COUNT` and a\n`RAC_TELEMETRY_<NAME>_VALUES[]` array:\n\n| Table | Count | Wire field | Values |\n|---|---|---|---|\n| `RAC_TELEMETRY_EVENT_TYPE` | 106 | `event_type` | namespaced strings, e.g. `llm.generation.completed`, `auth.succeeded`, `stt.transcription.completed` |\n| `RAC_TELEMETRY_BACKEND` | 22 | `backend` / `framework` | `onnx, sherpa, llamacpp, foundation_models, system_tts, fluid_audio, coreml, whisperkit_coreml, mlx, qhexrt, neurt, tflite, executorch, mediapipe, mlc, pico_llm, piper_tts, swift_transformers, cloud, builtin, none, unknown` |\n| `RAC_TELEMETRY_MODEL_SOURCE` | 3 | `model_source` | `catalog, builtin, user` |\n| `RAC_TELEMETRY_PLATFORM` | 9 | `platform` | `ios, android, macos, windows, linux, web, tvos, watchos, visionos` |\n| `RAC_TELEMETRY_SDK_BINDING` | 9 | `sdk_binding` | `swift, kotlin, flutter, react-native, web, electron, python, cli, cpp` |\n| `RAC_TELEMETRY_BATTERY_STATE` | 4 | `battery_state` | `charging, full, unplugged, unknown` |\n\n**The rule, verbatim from `add_vocabulary_string()` in `telemetry_json.cpp`:** if a value is\nnon-null but not present in its table, the field is dropped to `null` and the *rest of the\nevent still ships*. This is by design (a bad `backend` string must not cost the whole\nobservation) but it means:\n\n- The backend never sees the bad value, never 422s, never quarantines anything — there is\n  no error path anywhere for this. A field just isn't there.\n- The only client-side signal is a `RAC_LOG_WARNING(\"Telemetry\", \"Dropping %s: '%s' is not\n  in the published vocabulary\", ...)` log line — easy to miss unless you're specifically\n  watching debug logs.\n- `platform`, `sdk_binding`, and `battery_state` are stamped by the manager itself (§1), not\n  supplied by a binding's call site, so in practice only `backend`/`framework` (from the\n  engine/model layer) and `event_type` (only reachable via the proto path, checked at CI\n  time — see §5) are realistic places a new value can silently disappear.\n- `test_telemetry_extraction.cpp`'s vocabulary block (`\"vocab-1\"`/`\"vocab-2\"` cases) is the\n  concrete regression guard: it sends `framework = \"LlamaCpp\"` (a real spelling that used to\n  reach production) and asserts the string never appears on the wire while `model_id` still\n  does. If you touch `add_vocabulary_string()` or add a new vocabulary-gated field, extend\n  that test rather than trusting a manual check.\n\nHistorical motivation (from the header's own comment): before this table existed, stored\nproduction rows had four spellings of `llama.cpp`, three of ONNX, and binding names like\n`\"react-native\"` recorded as a `platform` — this file exists specifically to make that class\nof drift fail in this repo instead of being discovered in a database nobody was watching.\n\n## 3. Presence vs. absence: the \"measured zero\" contract\n\nBefore the fix documented in `telemetry_json.cpp`'s header, extractors read proto3 scalar\nfields directly — which report `0` for a field that was never set — and the old serializer\n(`add_int`/`add_double`) *skipped* zero values to keep payloads small. Result: a genuinely\nmeasured `output_tokens = 0` (an empty completion) and an unmeasured `tokens_per_second`\nboth arrived on the wire as absent, indistinguishable from each other.\n\nThe fix is `has_x`-gated serialization. Two helper families in `telemetry_json.cpp`:\n\n- `add_int_or_null(key, value, is_valid)` / `add_double_or_null(key, value, is_valid)` —\n  emit the real value (including `0`) when `is_valid` is true, else the JSON literal `null`.\n  Non-finite doubles (`NaN`/`Inf`) always degrade to `null` regardless of `is_valid`, because\n  JSON has no such literal and one bad value must not corrupt the whole batch.\n- The `is_valid` argument is a real `has_x`/sentinel check per field, not a blanket flag —\n  e.g. `payload->has_processing_time_ms != RAC_FALSE`, `payload->total_memory > 0`,\n  `payload->battery_level >= 0`.\n\n**What to check to confirm this stayed fixed** (this is exactly what\n`test_telemetry_extraction.cpp`'s presence block does — see §4): send an LLM completion\nevent with `output_tokens = 0` and `input_tokens = 0` explicitly set, and\n`tokens_per_second`/`context_length` left unset. Assert the wire body contains\n`\"output_tokens\":0` and `\"input_tokens\":0` as literal numbers, and `\"tokens_per_second\":null`\n/ `\"context_length\":null` — not all four as `null`, and not all four as `0`. If a future\nrefactor reverts to plain `add_int`/`add_double` for any of these fields, that regression\ntest is the thing that catches it; grep `telemetry_json.cpp` for `add_int(` / `add_double(`\n(the non-`_or_null`/non-`_always` forms) if you suspect a field regressed — those two still\nsilently skip zero by design and must never be used for a field where `0` is meaningful.\n\n## 4. Build, test, and what each telemetry test asserts (macOS)\n\n```bash\ncmake --preset macos-debug\ncmake --build build/macos-debug\nctest --preset macos-debug --output-on-failure -R 'telemetry|device_identity'\n```\n\nOr the non-preset form (`core/AGENTS.md`'s documented path):\n\n```bash\ncmake -B build -DRAC_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Debug\ncmake --build build\nctest --test-dir build --output-on-failure\n```\n\n`telemetry_extraction_tests` and `device_identity_tests` are registered in\n`core/tests/CMakeLists.txt`; `rcli_telemetry_live_tests` is registered separately in\n`rcli/tests/CMakeLists.txt`. All three are hand-rolled assertion runners — `CHECK()` in\nsome files, `ASSERT_EQ`/`ASSERT_TRUE` (from `core/tests/test_common.h`) in others — never\nGoogleTest:\n\n| ctest name | Binary | What it asserts |\n|---|---|---|\n| `telemetry_extraction_tests` | `test_telemetry_extraction` | Full proto→JSON pipeline per modality (LLM token counts/`tokens_per_second`, STT NaN-confidence never leaks as the string `\"nan\"`, TTS `characters_per_second`, embeddings `total_tokens`/`batch_size`/`embedding_dimension`, LoRA failure-path fields, RAG retrieval counts, VLM image/timing fields, live device-state stamping); the presence/measured-zero contract (§3); the closed-vocabulary drop rule (§2); and an HTTP-retry backoff (a failed batch is re-sent only after >5s, never before). Requires `RAC_HAVE_PROTOBUF` — the test binary itself no-ops with exit 0 if commons wasn't built with protobuf support. |\n| `device_identity_tests` | `test_device_identity` | Every branch of `rac_device_get_or_create_persistent_id()`'s resolution chain (§6) against a mocked platform adapter — cache hit, vendor-ID fallback, fresh-UUID generation, and that a `secure_set` failure doesn't silently drop the ID. |\n| `rcli_telemetry_live_tests` | `test_rcli_telemetry_live` | **Opt-in, always registered but a no-op by default.** Runs only with `--live` *and* `RUNANYWHERE_BASE_URL`/`RUNANYWHERE_API_KEY` set; otherwise prints \"skip\" and exits 0 — a green ctest run for this target proves nothing by itself. When live, it runs `rcli::bootstrap()` (real API-key → device-register → JWT handshake), sends one real event per modality (LLM, embeddings, RAG, VLM, LoRA-failure) through the real HTTP transport, and fails on any non-2xx response — this is the test that would catch a strict-schema `422 extra_forbidden` from field drift against the real V2 endpoints. |\n\nRun it live (§7 has the full local-backend flow):\n\n```bash\nRUNANYWHERE_BASE_URL=https://<backend-origin> RUNANYWHERE_API_KEY=<key> \\\n  ./build/macos-debug/rcli/tests/test_rcli_telemetry_live --live\n```\n\n`test_platform_bridge` (same `CMakeLists.txt`, adjacent target) is the wider companion\ncheck — \"every value a binding supplies must reach the wire\" — covering the platform half\nof the same two flat C structs (`rac_client_info_t`/`rac_device_registration_info_t`) for\nevery SDK at once; run it alongside if you're touching client-info fields rather than\ntelemetry event fields specifically.\n\n## 5. Regenerating the vocabulary header, and the drift gate\n\n`rac_telemetry_vocabulary.h` (§2) is generated from `idl/http/sdk-openapi.json` — the\ndevice-facing OpenAPI subset copied in from the private backend repo. **How that file gets\nupdated on the backend side, and how it crosses into this repo, is the monorepo's\n`openapi-contract` skill's job — read that by name in the other repo rather than\nre-deriving the steps here.** From inside this repo, once `idl/http/sdk-openapi.json` is\ncurrent:\n\n```bash\npython3 idl/codegen/generate_telemetry_vocabulary.py          # regenerate the header\npython3 idl/codegen/generate_telemetry_vocabulary.py --check  # CI drift check; exit 1 if stale\n```\n\n`.github/workflows/telemetry-vocabulary-drift.yml` runs three checks on every push to\n`main`/`development` and on any PR touching `idl/http/**`, the codegen script, or the\ntelemetry source/header dirs:\n\n1. **Generated header is current** — the `--check` command above.\n2. **Every event type the core can emit is in the vocabulary** — a Python check that parses\n   `proto_event_type_string()`'s body out of `telemetry_manager.cpp` via regex, unions in\n   the two prefix-expansion families (`stt.model.*`/`tts.voice.*`/`llm.model.*` and\n   `llm*`/`vlm*` generation suffixes), and diffs against `TelemetryEventType`'s enum in\n   `idl/http/sdk-openapi.json`. Anything emittable but unpublished fails the job.\n3. **Every framework string the core can emit is in the vocabulary** — same technique\n   against `framework_proto_to_string()` and `framework_to_string()` vs. `TelemetryBackend`.\n\n**A documented drift you will hit if you trust the wrong file**: `idl/http/README.md`'s own\n\"What it pins\" table names the schema for the `framework`/`backend` field as\n`TelemetryFramework` — that's stale. The actual generator\n(`idl/codegen/generate_telemetry_vocabulary.py`'s `VOCABULARIES` dict) and the header it\nproduces both say `TelemetryBackend`. Trust the generator and the generated header's own\n`// Published as X` comments over that README table.\n\n## 6. Per-binding device identity: where it's created, where it's persisted\n\nThe resolution chain itself is centralized in commons —\n`core/src/infrastructure/device/device_identity.cpp`\n(`rac_device_get_or_create_persistent_id`, tested by `device_identity_tests`, §4) — and is\nthe same fixed order for every binding:\n\n1. `adapter.secure_get(\"com.runanywhere.sdk.device.uuid\")` — if found and non-empty, use it,\n   full stop.\n2. `adapter.get_vendor_id` (if the platform adapter provides one) — use it, then persist it\n   via `secure_set` so step 1 hits next boot.\n3. Generate a fresh RFC-4122 v4 UUID, persist it via `secure_set`.\n\nWhat differs per binding is **only** the `secure_get`/`secure_set` backing store — an\nunstable backing store (one that doesn't survive a real app restart, or that resolves to a\ndifferent physical location than the read path expects) is exactly the bug class that\ninflates device counts, because step 3 fires again on every \"cache miss\":\n\n| Binding | Backing store | Where |\n|---|---|---|\n| Swift (iOS/macOS) | Keychain, service `com.runanywhere.sdk` | `bindings/swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+PlatformAdapter.swift` (`platformSecureGetCallback`/`platformSecureSetCallback`) |\n| Kotlin (Android) | Android Keystore AES-256-GCM key, ciphertext in a no-backup app file (not `SharedPreferences`) | `bindings/kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/security/AndroidKeychainManager.kt` (`NoBackupCiphertextStore`), wired through `CppBridgePlatformAdapter.PlatformSecureStorage` |\n| Flutter | Native Keychain/Keystore helper via FFI symbols `ra_flutter_secure_storage_{store,retrieve,delete}` | `bindings/flutter/packages/runanywhere/lib/native/dart_bridge_secure_storage.dart`; the device-UUID key constant is duplicated (deliberately, per its own comment) as `_keyDeviceUUID = 'com.runanywhere.sdk.device.uuid'` in `dart_bridge_device.dart` |\n| React Native (Android) | `SecureStorageManager` (native Android side) via `PlatformAdapterBridge.kt`'s `secureGet`/`secureSet` | `bindings/react-native/packages/core/android/src/main/java/com/margelo/nitro/runanywhere/PlatformAdapterBridge.kt` |\n| Web | `localStorage`, key `rac_sdk_plaintext_com.runanywhere.sdk.device.uuid` — **plaintext**, no OS-level encryption equivalent exists in a browser; restricted by convention to non-sensitive IDs | `bindings/web/packages/core/src/runtime/PlatformAdapter.ts` (`registerSecureGet`/`registerSecureSet`); also has a separate `stableVendorId()` fallback under key `rac_sdk_plaintext_vendor_id` |\n| Electron (macOS/Linux) | Flat file under a secure directory, path-traversal-guarded (`fs::path(key)` must not be absolute or contain `..`) | `bindings/electron/native/posix_platform_adapter.cpp` (`posix_secure_get`/`posix_secure_set`) |\n| Electron (Windows) | Windows DPAPI (`CryptProtectData`, current-user scope, no UI) | `bindings/electron/native/win32_platform_adapter.cpp` (`win_secure_get`/`win_secure_set`) |\n\nThe key string itself — `com.runanywhere.sdk.device.uuid` — is intentionally the same\nliteral across Swift, Flutter, and RN (each source comments cross-reference the other two by\nname), so grepping for it is a fast way to confirm a new binding is reading/writing the\n*same* key it thinks it is, not a typo'd sibling.\n\n**How to verify stability across a real restart** (not just \"the code looks right\"):\n\n1. Run the binding once, capture the emitted `device_id` from a real request body (§7) or\n   from `POST /api/v1/devices/register`'s response.\n2. Fully terminate the process (not just background it — on mobile this matters, a\n   backgrounded app can keep its in-memory device-id cache alive without ever touching\n   the read path again).\n3. Relaunch and repeat step 1.\n4. The `device_id` must be byte-identical. If it changed, the break is almost always one of:\n   the backing store not surviving process death (e.g. an in-memory-only stub used during\n   development), a key-name mismatch between the write and read paths, or (Android\n   specifically) `Context.deleteSharedPreferences` being invoked on the wrong file — see\n   `AndroidKeychainManager.kt`'s constructor, which deletes a hardcoded *legacy* prefs name\n   on every construction; if that name were ever widened to match the real store, every\n   Android launch would look like a fresh device.\n\nTwo-in-one launches on the *same physical device* (e.g. two emulator instances, or a\nsame-device reinstall vs. a real fresh install) are a different, real-world failure mode\nfrom a code bug — don't conflate an intentional reinstall's fresh ID with instability.\n\n## 7. Point a local build at a local backend and prove an event landed\n\n`rcli` is the fastest way to exercise the full wire path without a mobile/browser\ntoolchain — it links `rac_commons` directly and speaks real HTTP via curl.\n\n```bash\ncmake --preset rcli-macos-release   # or rcli-linux-release on Linux\ncmake --build build/rcli-macos-release --target rcli -j \"$(sysctl -n hw.logicalcpu)\"\n```\n\n**Keyless (development environment) — no API key, works against any backend that allows\nanonymous V2 telemetry:**\n\n```bash\n./build/rcli-macos-release/rcli/rcli --environment development \\\n  --base-url http://127.0.0.1:8000 \\\n  telemetry blast --processing-ms 42.5 --session-id my-check \\\n  --input-tokens 128 --output-tokens 256\n```\n\nThis is exactly the shape `scripts/ci/oss_keyless_telemetry_blast.sh` runs in CI against the\npublic staging backend — `--environment development --base-url <origin> telemetry blast\n...` with no `--api-key`. `telemetry blast` sends one representative event per modality (all\n12) and prints a `MODALITY | RESULT | STATUS | RECEIVED | STORED | SKIPPED` table parsed\ndirectly from the backend's own batch response — **the `STORED` column is authoritative\nserver-side confirmation**, not just an HTTP 2xx. `run_telemetry_blast()`'s own exit-code\nlogic (`rcli/src/commands/cmd_telemetry.cpp`) does fold `stored >= count` into its overall\npass/fail, so a genuine storage shortfall on any modality does make the process exit\nnon-zero, not just a transport error — but the exit code is one bit for all 12 modalities\ncombined, so a non-zero exit still requires reading the table to know *which* modality\nfailed and *why* (a `network error` status is a transport problem; an `HTTP 200` row with\n`STORED` under `RECEIVED` is a row-level rejection, e.g. a vocabulary value the backend\ndidn't recognize — a real bug to chase, not the same failure class at all).\n\n**Authenticated (production shape) — for exercising the login handshake too:**\n\n```bash\n./build/rcli-macos-release/rcli/rcli --environment production \\\n  --base-url https://<backend-origin> --api-key <key> \\\n  auth login\n./build/rcli-macos-release/rcli/rcli --environment production \\\n  --base-url https://<backend-origin> --api-key <key> \\\n  telemetry emit --modality llm --count 3 --input-tokens 128 --output-tokens 256\n```\n\n`auth login` only has a path in `production` — `development`'s keyless mode has no login\nstep by design (see `rcli/src/commands/cmd_auth.cpp`'s own subcommand help text).\n\n**To inspect the actual request body**, not just the summary table, add `--json` for\nmachine-readable CLI output and `-v` for debug logging (the debug log includes the\nserialized JSON per batch before it goes on the wire) — or point `--base-url` at a throwaway\nlocal capture endpoint (any script that logs the raw POST body) before pointing it at a real\nbackend, to sanity-check field shape offline first.\n\n**If you have the private backend repo checked out locally too**, running it yourself\n(`cd backend && uv run uvicorn main:app --reload`, per that repo's own README) and pointing\n`--base-url` at `http://127.0.0.1:8000` gets you the strongest possible evidence: query the\nSQLite/Postgres row directly after a blast and diff it field-for-field against what you sent\n— the monorepo's own `telemetry-e2e` skill (its integration suite, hermetic pytest fixtures,\nand `test_telemetry_conformance.py`'s vocabulary/presence/identity assertions against stored\nrows) is the authoritative reference for that side; don't re-implement its DB assertions\nhere, just point at it.\n\n## Evidence bar, restated\n\n- Compiling `rcli` or a binding: **not evidence**.\n- `ctest -R telemetry_extraction_tests` green: **not evidence of a real call site** — only\n  of the serializer in isolation.\n- `rcli telemetry blast` exiting 0: real signal (its exit code does fold in the backend's\n  `stored >= count` check, §7) but still worth pairing with the per-modality\n  `STORED`/`SKIPPED` table in your report — the exit code can't tell you *which* modality\n  or *why* a shortfall happened.\n- A captured request body showing the right endpoint, the right keys, correct\n  presence/absence per §3, and no off-vocabulary values per §2 — **evidence of correct\n  emission**.\n- That request body's values matching a stored row (via the backend's own\n  `events_stored` count at minimum, a direct DB read at best) — **evidence of correct\n  end-to-end delivery**, the only tier worth reporting as \"telemetry works.\"\n",".agents/skills/sdk-test-starters/SKILL.md":"---\nname: sdk-test-starters\ndescription: Run a REAL end-to-end inference smoke test (model download + load + generate, with visible output) on every RunAnywhere starter app — iOS, macOS/Swift, Android, Web, Electron — after a runanywhere-sdks release. Build success alone is never sufficient evidence.\n---\n# SDK Test Starters\n\nPost-release validation runbook for the five RunAnywhere SDK consumer apps. This\nskill exists because \"it built\" has been reported as \"tested\" before, and that is\nnot evidence of anything except that the compiler ran.\n\n## 0. The evidence bar (applies to every app, no exceptions)\n\nFor each app you must produce, and be able to state explicitly:\n\n1. **Version proof** — print/log the actual resolved SDK version from inside the\n   running app (a startup log line, a version screen, an `swift package show-dependencies`\n   / `./gradlew dependencies` / `npm ls @runanywhere/...` output, etc). Never assume\n   a version bump \"took\" just because you edited a manifest.\n2. **Real model acquired** — a real (small) model downloaded fresh, or confirmed\n   already cached, by the app itself (not a file you hand-placed unless that IS the\n   app's supported flow).\n3. **Load** — the model actually loads (watch for a load-succeeded log/state, not\n   just \"no crash\").\n4. **One real inference call** — LLM `generate()` is almost always the easiest\n   modality to drive; use it unless another modality is genuinely less work for\n   that specific app.\n5. **Visible output** — capture the actual generated text (or audio/image) and\n   quote it in your report. \"It responded\" is not enough — show what it said.\n6. **Logs reviewed** — read the console/log stream for errors and warnings and\n   explicitly explain any that appear. Do not silently drop a stack trace because\n   the app \"still produced output.\"\n\nReporting an app as tested from a build/compile success alone is a skill violation.\nIf you could not get past build, that is a BLOCKED report (see §8), not a PASS.\n\n## 1. Locating each app's real local checkout\n\nThis machine accumulates many differently-named copies of the same repos over time\n(worktrees, experiment clones, old branches, scratch dirs). **Never trust a\ndirectory name.** Before touching anything in a candidate directory:\n\n```bash\ngit remote get-url origin          # must be https://github.com/RunanywhereAI/<repo>.git\ngit status --short --branch        # what branch, what's dirty, is it someone else's WIP?\n```\n\nAs of this writing, the known-good local checkouts for this user are here (verify\nthis each time — do not hardcode it further than \"check this location first\"):\n\n```\n~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-ios       -> RunanywhereAI/runanywhere-ios.git\n~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-android   -> RunanywhereAI/runanywhere-android.git\n~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-web       -> RunanywhereAI/runanywhere-web.git\n~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-electron  -> RunanywhereAI/runanywhere-electron.git\n```\n\nThe macOS/Swift case has no separate consumer repo — see §4, it lives inside\n`~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks` (confirm with\n`git remote get-url origin` there too — this machine also has stray full-monorepo\nclones elsewhere, e.g. under `~/development/hard/ambient/`, that are NOT the one to\nuse unless you've verified it's the intended checkout).\n\nThese starter checkouts are commonly sitting on a feature branch, not `main` — e.g.\nat last check `runanywhere-ios` and `runanywhere-android` were on\n`add-gemma4-qwen3.6-qwen3.8-models` and `runanywhere-web` on `add-supertonic-3-tts`.\nThat is fine to test FROM, but say so in your report, and do not `git checkout` away\nfrom a branch that has uncommitted work you didn't create — if `git status --short`\nshows dirty files you don't recognize, stop and ask rather than stashing/resetting.\n\nIf genuinely unsure which directory is real, `git clone` a fresh one into the\nscratchpad rather than guessing — a wrong guess wastes far more time than a clone.\n\n## 2. Cross-repo facts this skill assumes\n\n- SDK version SoT: `core/VERSION` in `runanywhere-sdks`, bumped everywhere by\n  `scripts/release/sync-versions.sh <version>`.\n- Release publish is `.github/workflows/release.yml` (dispatch or `v*.*.*` tag).\n- **Never** treat a build success against `RUNANYWHERE_USE_LOCAL_NATIVES=1` (or any\n  monorepo-local staged artifact) as proof the *public* release works — see §4.\n- QHexRT/QNN material must never appear in a public artifact — release.yml\n  hard-fails on it. If anything you're testing surfaces a QHexRT asset in a public\n  package, stop and flag it, don't route around it.\n\n## 3. iOS — `runanywhere-ios`\n\nSDK pin lives in `Package.swift`:\n\n```swift\n.package(url: \"https://github.com/RunanywhereAI/runanywhere-swift.git\", from: \"0.20.19\")\n```\n\nThis only resolves once `runanywhere-swift` (the generated Swift-only SPM\ndistribution repo — see `~/.claude/skills/sdk-publish/SKILL.md` §4) is\nactually tagged at the target version. Steps:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-ios\n# bump the `from:` version in Package.swift to the target release, then:\nswift package update            # or open in Xcode and let it resolve\n./scripts/build_and_run_ios_sample.sh simulator \"iPhone 16 Pro\"\n```\n\n`./scripts/verify.sh` is the CI-equivalent gate: it resolves the package remotely\nand runs a full `xcodebuild` against the iOS Simulator — use it to prove the\nresolve is clean (it fails if `Package.resolved` ends up dirty, i.e. the committed\npin was stale). `./scripts/smoke.sh` is a fast, non-compiling grep-based check —\nit does NOT count as evidence for this skill's bar, only `build_and_run` +\nwatching real output does.\n\nDrive one real `generate()` call through the app UI (or the equivalent debug\naction if the sample exposes one) against a small downloaded model, then watch the\nruntime log stream for the version line, the model download/load, the generation,\nand any errors:\n\n```bash\nlog stream --predicate 'subsystem CONTAINS \"com.runanywhere\"' --info --debug\n```\n\nMost loggers use `com.runanywhere.RunAnywhereAI`; a few use plain\n`com.runanywhere`; the SDK logs under its own subsystem — the `CONTAINS` match\ncovers all of them. On a physical device use `idevicesyslog | grep \"com.runanywhere\"`\ninstead.\n\n## 4. macOS/Swift — no separate repo, use the in-tree example or rcli\n\nThere is no `runanywhere-macos` consumer repo. Two in-monorepo options, both inside\n`~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks`:\n\n- `bindings/swift/example/` — a minimal SwiftPM example app/executable.\n- `rcli/` — the C++ core's CLI, `rac_*` C ABI consumer.\n\n**The critical gotcha**: the repo's own `Package.swift` is fail-closed toward the\n*real published release* — it resolves the remote `binaryTargets` unless you\nexplicitly opt into locally staged XCFrameworks with\n`RUNANYWHERE_USE_LOCAL_NATIVES=1`. Every doc/script example in this repo sets that\nenv var (because local dev wants fast iteration against in-progress binaries) —\nbut that means copy-pasting those examples verbatim tests your LOCAL staged build,\nnot the actual public release artifact. To prove the published release works:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks/bindings/swift/example\n# DO NOT set RUNANYWHERE_USE_LOCAL_NATIVES — leave it unset so Package.swift\n# resolves the real remote release binaries for the target version.\nswift build\nswift run runanywhere-minimal \"Name three colours.\"\n```\n\nIf the example was previously built/run with the env var set, `swift build` may\nreuse a stale resolve — run `swift package reset` (or delete `.build/`) first if\nyou're not sure which mode the last resolve used.\n\nFor rcli instead (also validates the real published core):\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks\nrcli models download qwen3          # ~639MB, small enough for a quick smoke test\nrcli llm generate --model qwen3 \"Reply with exactly: RCLI WORKS\" --reasoning off\n```\n\n`rcli models list --all` shows the rest of the built-in catalog\n(`whisper-tiny`, `smolvlm2`, `piper`, …) if LLM generate isn't the right modality\nfor what you're verifying.\n\n## 5. Android — `runanywhere-android`\n\nVersion pin: `gradle/libs.versions.toml` has ONE shared catalog entry:\n\n```toml\nrunanywhere = \"0.20.19\"\n...\nrunanywhere-sdk      = { group = \"io.github.sanchitmonga22\", name = \"runanywhere-sdk\",              version.ref = \"runanywhere\" }\nrunanywhere-llamacpp = { group = \"io.github.sanchitmonga22\", name = \"runanywhere-llamacpp\",          version.ref = \"runanywhere\" }\nrunanywhere-onnx     = { group = \"io.github.sanchitmonga22\", name = \"runanywhere-onnx\",               version.ref = \"runanywhere\" }\nrunanywhere-qhexrt   = { group = \"io.github.sanchitmonga22\", name = \"runanywhere-qhexrt-android\",     version.ref = \"runanywhere\" }\n```\n\n**CRITICAL — do not bump this block as one unit.** `runanywhere-qhexrt-android` is\npermanently frozen at whatever version it was last published at before being\nexcluded from the public Maven train; by policy it can never move forward via\npublic Maven again. Verify the actual frozen version FRESH every time (do not\ntrust a remembered number — it doesn't change, but don't skip the check):\n\n```bash\ncurl -s https://repo1.maven.org/maven2/io/github/sanchitmonga22/runanywhere-qhexrt-android/maven-metadata.xml\n```\n\nThe other three artifacts (`runanywhere-sdk`, `-llamacpp`, `-onnx`) CAN and should\nmove to the new release version. This means the single `runanywhere` catalog ref\nmust be **split into two entries** — one at the new version for sdk/llamacpp/onnx,\none pinned to the frozen qhexrt version — not bumped as a block.\n\n**Before trusting a mixed pin works**: verify `RAC_PLUGIN_API_VERSION` (the\nplugin ABI version constant in the C++ core, exposed identically to Kotlin) is\nunchanged between the old public SDK version and the new one. If it changed, mixing\na newer sdk/llamacpp/onnx with the frozen qhexrt version is NOT safe — this is a\nhuman decision (needs either an ABI-compat shim or dropping qhexrt from this build),\nnot something to force through.\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-android\n./gradlew :app:assembleDebug\n./gradlew :app:installDebug        # installs to a connected emulator/device\nadb logcat | grep -i runanywhere   # or a more specific tag if the app uses one\n```\n\nLaunch the app, drive one real `generate()` call against a small downloaded model\n(the app's own model picker/download flow), confirm the resolved SDK version is\nlogged, and review logcat for errors before reporting a PASS.\n\n## 6. Web — `runanywhere-web`\n\n`package.json` has 4 `@runanywhere/*` deps pinned with a caret range:\n\n```json\n\"@runanywhere/proto-ts\":    \"^0.20.19\",\n\"@runanywhere/web\":         \"^0.20.19\",\n\"@runanywhere/web-llamacpp\":\"^0.20.19\",\n\"@runanywhere/web-onnx\":    \"^0.20.19\"\n```\n\nBump all 4 to the new version, then:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-web\nnpm install\nnpm run build\nnpm run dev      # vite --host localhost --port 3000 --strictPort\n```\n\nThe dev/preview server is what supplies COOP/COEP headers required for\n`SharedArrayBuffer` — WASM inference will silently fail (or refuse to init\nthreads) without them, so don't swap in a bare static file server.\n\n`~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks/bindings/web/AGENTS.md`\nhas the authoritative Validation section (its own heading is `## Validation`) —\nfull checklist:\n\n1. Fresh browser context.\n2. Example app served with COOP/COEP headers.\n3. Model download.\n4. Model load.\n5. Real browser inference for the target modality.\n6. Logs/screenshots reviewed.\n\nIts \"Required release gates\" (run before browser validation) from the same repo:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks/bindings/web\nnpm run typecheck && npm run lint && npm run test && npm run build\nnpm run test:browser\nnpm run test:browser:release\ncd example && npm run typecheck && npm run build\n```\n\nStatic gates passing is not the smoke test — you still need an actual browser\nwatching real inference happen against the starter app. Prefer a real browser\nautomation tool (`claude-in-chrome` MCP tools, or Playwright — the maintained\nsuites under `bindings/web/tests/browser/` already use `playwright.config.ts`)\nover curling the served page: WASM inference genuinely needs a browser JS engine.\nLoad the served starter app, download/load a small model, trigger generate, and\nread the actual rendered output text plus the browser console — do not infer\nsuccess from an HTTP 200 on the page load.\n\n## 7. Electron — `runanywhere-electron` (SPECIAL CASE)\n\n**Do not assume a fresh SDK publish is available for this app.** Electron's own\nnpm packages (`@runanywhere/electron`, `-llamacpp`, `-onnx`, `-sherpa`, `-qhexrt`,\n`-neurt`) are on a SEPARATE, manual-publish pipeline — NOT part of `release.yml`\n(grep confirms zero mentions of these package names there). This separation is a\n**confirmed, deliberate** repo-owner decision, not a gap. Check\n`~/.claude/skills/sdk-publish/SKILL.md` §6 before assuming anything moved.\nElectron is **no longer WIP on a branch** — the old \"in-progress on\n`smonga/electron_upgrade`\" note is gone from the root `AGENTS.md`/`CLAUDE.md`\nentirely as of this checkout; `bindings/electron/` is a fully merged SDK on `main`.\nAs of `fix/electron-cicd-staging-url-npu-ane`,\n`.github/workflows/electron-native-package.yml` builds every backend for real and\npackages every tarball — but still does not publish; verify against a live run\nrather than assuming build automation status either way.\n\nTest the starter against whatever version it is CURRENTLY pinned to unless a human\nhas explicitly confirmed a newer Electron publish is ready and intended:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-electron\ncat package.json | grep '@runanywhere/electron'   # confirm current pin before touching it\n```\n\n**`@runanywhere/electron-qhexrt` being public on npm is a CONFIRMED, settled\npolicy decision** (repo owner, 2026-08-20), not a contradiction to flag — it\ncontains real Hexagon NPU binaries (`libQnnHtpV81Skel.so`, `QnnHtp*.dll`,\n`rac_commons.dll`, `runanywhere_qhexrt.dll`) on purpose. The QHexRT-stays-private\npolicy enforced elsewhere in this repo family is specifically about the\nGitHub-Release-side asset set (Android etc.), a different channel. Only re-raise\nthis if you find evidence the policy changed.\n\nTwo layers to actually exercise, do not conflate them:\n\n**(a) Cross-platform Electron JS + macOS native addon** — buildable and testable\nlocally on this Mac right now:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-electron\nnpm install\nnpm run build   # or the app's documented dev/start script — check package.json scripts\nnpm start        # or equivalent — launch the actual Electron app\n```\n\nDrive one real `generate()` call in the running app, watch its console/log output\nfor the resolved SDK version, model download, load, and generation, and read any\nerrors rather than ignoring them.\n\n**(b) Genuine Windows-native behavior** — a macOS build proves NOTHING here\nregardless of how clean the cross-platform code looks; the native addon and any\nWindows-only DLLs must be exercised on real Windows.\n\nThe team keeps a **Windows ARM64 test machine reachable over SSH**. Its hostname,\nuser, key, and VPN/mesh node name are operator-specific and deliberately not\nrecorded in this repo — read them out of the operator's own `~/.ssh/config` (look\nfor the Windows/ARM64 host entry) and use that alias below wherever `$WIN_BOX`\nappears. If no such entry exists, the box is not configured for you: say so and\nask, rather than guessing at a hostname.\n\n```bash\nWIN_BOX=<the ssh alias from ~/.ssh/config>\n```\n\n**Always check reachability first, never just try SSH and retry on timeout.** The\nbox lives on a private mesh VPN and is frequently asleep, so start by asking the\nVPN client whether the node is up (e.g. `tailscale status`, matching on the host\nentry's `HostName`) rather than opening a connection.\n\nIf the node reports `offline, last seen ...`, the box is asleep/powered off.\n**A connection timeout here means \"device asleep,\" not \"config is wrong.\"** Tell\nthe human and ask them to wake it; do not loop retrying SSH. Only once the node\nshows a live/idle state, confirm with:\n\n```bash\nssh -o ConnectTimeout=8 \"$WIN_BOX\" \"whoami\"\n```\n\nThen, with the repo already cloned there:\n\n```bash\nssh \"$WIN_BOX\" \"cd <repo-path-on-that-box>; git pull; npm install; npm run build\"\n# then run whatever launches the Windows Electron build and drive a real generate call\nscp \"$WIN_BOX\":<remote-log-or-artifact-path> ./local-copy   # pull back evidence to inspect\n```\n\nDo not claim Windows-specific validation happened unless you actually obtained a\nshell on that box and ran something real there — a clean macOS build is not\nsubstitute evidence.\n\n**`node_modules/electron` on this box silently tracks whichever arch last ran `npm\ninstall`, not which Node launches the test.** The x64 (llamacpp/onnx/sherpa) and\narm64 (QHexRT) lanes share ONE `runanywhere-electron` checkout/`node_modules` tree.\nPrepending an x64 Node to `PATH` and running `npx playwright test` does NOT make\nElectron itself x64 — `require('electron')` always resolves the single installed\n`node_modules/electron/dist/electron.exe`, and whichever arch that binary actually\nis determines which `prebuilds/win32-<arch>/` directory the running app looks in,\nregardless of `scripts/use-sdk.mjs local`'s staging (which itself is arch-aware\nbased on the *launching* Node, so it can correctly report \"staged win32-x64\" while\nthe process that will actually run is arm64 — a silent mismatch). Verify with:\n`powershell -Command \"$p=[IO.File]::ReadAllBytes('...\\electron.exe'); $peOff=\n[BitConverter]::ToInt32($p,60); [BitConverter]::ToUInt16($p,$peOff+4)\"` — `43620`\n(`0xAA64`) is ARM64, `34404` (`0x8664`) is x64. If it's the wrong arch (e.g. a prior\nsession's `npm install` for the other lane silently overwrote it), a genuine re-test\nof the other lane needs a FULLY SEPARATE checkout/`node_modules` tree installed\nunder the matching-arch Node — don't assume PATH-prepending alone is sufficient, and\ndon't trust `use-sdk.mjs status`'s \"platform: win32-x64\" output as proof the running\nElectron process is actually x64.\n\n**NPU hardware is a scarce, single-owner resource — serialize, never parallelize.**\nThis box has ONE physical Hexagon NPU. `engines/qhexrt/qhexrt_session.cpp`'s\n`session_open()` has a load-bearing comment on this exact failure mode: the HTP\nexposes a small number of protection domains with a per-PD context ceiling, so a\n*second* concurrent model load fails with `RAC_ERROR_BACKEND_INIT_FAILED`\n(\"Backend initialization failed\") — a fast (tens-of-ms), generic-looking failure\nthat is easy to mistake for a real regression. Confirmed once tonight: two agents\neach independently exercising QHexRT on this box at the same time produced exactly\nthis failure. Before concluding a QHexRT test genuinely failed, confirm nothing\nelse (another agent, a leftover Electron process, a stuck previous test run) is\nalso holding an NPU session open on this box — check for lingering `electron.exe`/\n`node.exe` processes from a prior run and kill them if orphaned, and never launch\ntwo NPU-touching verification passes concurrently on this machine.\n\n**\"Backend initialization failed\" has (at least) three distinct root causes —\ndon't stop at PD exhaustion.** Confirmed on 2026-08-17 after PD-exhaustion and\nstaleness were both ruled out (fresh rebuild, source-identical prebuilt, no other\nprocess holding a session): the *actual* bug was two compounding issues, both now\nfixed/documented —\n1. **Real source bug, fixed in PR #733**: `qhexrt_session.cpp`'s `runtime_acquire()`\n   called `qhx_runtime_create(nullptr, nullptr)` on every non-Android platform,\n   trusting the OS default DLL search order to find `QnnHtp.dll`/`QnnSystem.dll`.\n   On Windows that order never includes a dynamically-loaded sibling plugin's own\n   directory (`LOAD_WITH_ALTERED_SEARCH_PATH`, used to load `runanywhere_qhexrt.dll`\n   itself, only widens the search for *that* load call, not later `LoadLibrary`\n   calls made by code already running inside it) — so the QNN DLLs were invisible\n   even sitting right next to the plugin. Fixed by resolving the plugin's own\n   module directory (`GetModuleHandleExA`+`GetModuleFileNameA`) and passing\n   explicit paths, mirroring Android's existing `ADSP_LIBRARY_PATH` resolution in\n   the same file.\n2. **Operational staleness, NOT a script bug**: even after (1), the QNN runtime\n   DLLs actually staged into `@runanywhere/electron-qhexrt`'s\n   `prebuilds/win32-arm64` (`node_modules/.../electron-qhexrt/prebuilds/win32-arm64/`)\n   were from an old QAIRT build (~2.45.x vintage, Dec 2025 files) while the box's\n   installed QAIRT was 2.48.0.260626 — a real ABI-relevant version mismatch.\n   `bindings/electron/scripts/bundle-native.ts`'s `stageQnnRuntime()` just mirrors\n   whatever `RA_QNN_RUNTIME_DIR` points at with **no version check** — it's\n   correct-as-written; someone had pointed it at a stale neurun-side\n   `qairt-runtime-248`-named folder that was never refreshed. Fix: rebuild that\n   flat dir from the CURRENT QAIRT SDK's `lib/aarch64-windows-msvc/` (host DLLs:\n   `QnnHtp.dll`, `QnnHtpPrepare.dll`, `QnnHtpV81Stub.dll`,\n   `QnnHtpV81CalculatorStub.dll`, `QnnSystem.dll`) + `lib/hexagon-v81/unsigned/`\n   (DSP side: `libQnnHtpV81Skel.so`, `libqnnhtpv81.cat`), point\n   `RA_QNN_RUNTIME_DIR` at it, and re-run\n   `npm run bundle:native -- --package=qhexrt` from `bindings/electron/`.\n   **The already-published `@runanywhere/electron-qhexrt` npm package (as of\n   0.20.24) almost certainly ships the same stale DLLs** — this needs a\n   human decision (republish/patch) before claiming the published package works\n   on Windows NPU, separate from the source fix in PR #733.\n3. **PD exhaustion** (the pre-existing entry above) remains real and still applies\n   when two sessions genuinely contend for the one physical NPU — but it is not\n   the only explanation, and the log line to actually check first is the native\n   one: subscribe via `window.runanywhere.logging.records()` (after\n   `logging.setDebugMode(true)`) or add a temporary main-process log capture —\n   `app.process().stdout`/`stderr` do NOT carry it, because the addon runs in a\n   separate `utilityProcess.fork()` host, not the Electron main process, and\n   `RAC_LOG` is not an env var the logger reads (it does nothing). The generic\n   `[QHexRT] qhx_runtime_create failed (QNN libs unavailable?)` vs.\n   `qhx_model_load failed: ... N NPU model(s) already resident` log text is what\n   actually distinguishes cause (1)/(2) from cause (3) — always get that line\n   before diagnosing further.\n\n**Workflow gotcha:** if you orchestrate this via the Workflow tool, its agent()\ncalls do NOT reliably block-wait on a long-running remote build the way a direct\npoll loop does — observed once tonight where a workflow's own agents spawned an\ninternal Monitor/poller for the build log and then returned immediately,\nreporting \"still waiting,\" which let the whole workflow finish with nothing\nactually verified. For \"wait N tens-of-minutes for a real native build to\nfinish,\" use the top-level Monitor tool yourself (a shell poll loop over SSH\nchecking for a terminal BUILD_SUCCEEDED/BUILD_FAILED marker line) rather than\ndelegating the wait itself into a workflow agent's own judgment.\n\n### The Windows box's actual layout, and why it's more than a cross-platform check\n\n**This is a Hexagon-NPU-equipped Windows machine** (Snapdragon-class ARM Windows\ndevice, Copilot+ PC), not just a generic x86 Windows box. That makes it the one\nplace a QHexRT/Hexagon backend build+test can produce real device-only truth for\nWindows — matching the parent workspace's own \"x86 is a simulator, device-only\ntruth\" rule (see the top-level `Qualcomm/CLAUDE.md` and `QHexRT/CLAUDE.md`). Don't\ntreat this box as merely a Windows Electron smoke-test target; it's the only real\nHexagon NPU test rig reachable for this platform.\n\nPer the user (2026-08-17), under `~/Downloads/RunAnywhere/` on that box there are\nthree sibling repo checkouts at the same level — **verify each on connect, don't\nassume paths without checking, since this is relayed secondhand and may have\ndrifted**:\n- `runanywhere-sdks` — this monorepo, presumably already cloned; `git fetch && git\n  log -1 origin/main` to see how far behind it is before doing anything.\n- `runanywhere-electron` — the Electron starter app (same repo as\n  `RunanywhereAI/runanywhere-electron` elsewhere in this skill).\n- a third repo referred to verbally as \"the neuron/new-run inference engine for the\n  Hexagon NPU\" — almost certainly **`neurun`**, the QHexRT/NeuRT split-out repo\n  (see memory: \"neurun component split merged — QHexRT/ + NeuRT/ + shared/, root is\n  a closed 20-entry set enforced by a test\"). Confirm the actual directory name and\n  remote (`git remote get-url origin`) on connect rather than guessing from the\n  transcription.\n\n**Full task on this box (per explicit user instruction, not just a smoke test):**\n1. `git pull` (or fetch+fast-forward) all three repos to latest `main`.\n2. Rebuild the QHexRT/Hexagon NPU backend from `neurun` against this device's real\n   Hexagon NPU — this is the one environment where that build+run is meaningful\n   proof, not a simulator claim.\n3. Bump Electron's native dependencies to their latest versions — llama.cpp,\n   sherpa-onnx, ONNX Runtime, and whatever else `bindings/electron` pins — across\n   the repos involved. Treat this as a real dependency-bump task (check for\n   breaking API changes, don't just edit a version string and assume it builds).\n4. Update the model catalog for **all 5 starter apps across all 4 consumer repos**\n   (iOS, Android, Web, Electron — plus whatever the \"5th app\" split means for\n   macOS/Swift, see the rest of this skill) so the catalog is consistent everywhere,\n   not just on this Windows box.\n5. Run a real end-to-end inference pass on this box specifically (not just build\n   success) — both the general Electron/llama.cpp path AND the Hexagon NPU path via\n   `neurun`/QHexRT.\n6. Open PRs for whatever changed, in each affected repo. Do not merge them —\n   the user explicitly said they'll review.\n\nThis is real engineering work (native dependency bumps + an NPU backend rebuild),\nnot a mechanical version-string edit — expect it to take real time and to\ngenuinely need `QHexRT/CLAUDE.md` and `neurun`'s own docs read in full before\ntouching that backend, per the parent workspace's own instructions.\n\n## 8. Reporting\n\nFor each of the 5 apps (iOS, macOS/Swift, Android, Web, Electron), state exactly\none of these two outcomes — never blur them:\n\n- **FULLY VERIFIED** — with concrete evidence: the version string actually\n  resolved/logged, the model used, and the actual output produced (quote it).\n- **BLOCKED** — with the exact concrete reason, e.g.: the Windows ARM64 box's\n  mesh-VPN node is offline; Android emulator unavailable in this environment; App Store /\n  notarization credential missing; `runanywhere-swift` not yet tagged at target\n  version so `swift package update` can't resolve; electron-qhexrt policy conflict\n  needs a human call before proceeding further.\n\nA clearly reported partial/blocked result is far more valuable than an optimistic\nsummary that overstates what was actually observed. If you hit the known Electron\nqhexrt npm-publish policy conflict (§7), call it out in the report even if it\nwasn't the thing blocking your specific test — it's a standing issue every release\nnight should re-surface until a human resolves it.\n\n## Credentials referenced but never printed by this skill\n\nThese exist and are checked for presence only — never print their values:\n\n- Maven Central: `mavenCentral.username` / `mavenCentral.password`,\n  `signing.gnupg.keyName` / `signing.gnupg.passphrase` in `~/.gradle/gradle.properties`.\n- GPG signing key `CC377A9928C7BB18` — check with\n  `gpg --list-secret-keys --keyid-format LONG`.\n- macOS notarization profile `runanywhere-notary` — check with\n  `xcrun notarytool history --keychain-profile runanywhere-notary`; codesign\n  identity `Developer ID Application: RunAnywhere, Inc` — check with\n  `security find-identity -v -p codesigning`.\n- npm — `npm whoami` should already show a logged-in user.\n\nNone of the app-level smoke tests in this skill require touching these directly;\nthey matter only if a test path requires a fresh distro-repo publish (e.g.\n`runanywhere-swift` not yet tagged) — that's a separate concern of the\n`sdk-publish` skill, not this one.\n",".agents/skills/engine-plugin-authoring/SKILL.md":"---\nname: engine-plugin-authoring\ndescription: Step-by-step recipe for adding a new engine plugin under runanywhere-sdks/engines/, or adding a modality (e.g. TTS) to an existing engine (llamacpp, sherpa, onnx, cloud, mlx, neurt, qhexrt). Use when the task is \"add engine X\", \"wire up backend Y\", or \"make engine Z also serve <modality>\".\n---\n\n# Engine plugin authoring (`engines/`)\n\nTwo related runbooks: adding a brand-new engine plugin, and adding a modality to one\nthat already exists. Read `engines/AGENTS.md` first for the concepts these steps\nassume (op-table vtable, manifest, the valid-engine checklist, the 4-file skeleton,\nthe engine↔runtime rule). This skill is the \"how\", that file is the \"what/why\".\n\n**Invariant that applies to both procedures:** never invent a new modality just to\nship an engine — fill an existing `rac_engine_vtable_t` slot. Adding a brand-new\n*primitive* (a new `RAC_PRIMITIVE_*` + vtable slot) is a commons ABI change, covered\nby `core/AGENTS.md` → \"Adding a new capability interface\", not an engine change.\n\n---\n\n## Procedure A — add a new engine\n\nMirror **sherpa** (multi-modality, standalone `.so`) or **cloud** (single modality,\nno runtime) as your template; both are the cleanest references for the 4-file\nskeleton described in `engines/AGENTS.md`.\n\n1. **Create `engines/<name>/`** with the 4-file skeleton: `rac_plugin_entry_<name>.cpp`,\n   `rac_backend_<name>_register.cpp` (skip it if there's no extra bring-up, like\n   `neurt`), `rac_static_register_<name>.cpp`, plus your impl files.\n2. **Write the manifest** in `rac_plugin_entry_<name>.cpp`:\n   - `name` = snake_case identity (the library/framework you wrap, or your own\n     codebase unit — NEVER a modality name; see the naming rule in\n     `engines/AGENTS.md`).\n   - `primitives[]` = what you serve; fill the matching vtable slots; leave every\n     other slot explicit NULL (the all-NULL tripwire depends on this).\n   - `runtimes[]` — only devices your compute actually depends on (NULL for\n     cloud/HTTP engines) — see THE RULE in `engines/AGENTS.md`.\n   - `formats[]` = the `RAC_MODEL_FORMAT_ID_*` values you accept (NULL if there's\n     no local model file).\n   - `priority`, `availability` (PUBLIC/PRIVATE), `package_owner`/`package_name`.\n3. **Implement the op-table(s)** honoring the MODEL LIFECYCLE:\n   `create → initialize → use → cleanup → destroy` (see `engines/AGENTS.md` for the\n   exact signatures). Use `RAC_DEFINE_CREATE_ADAPTER(primitive, name)`\n   (`core/include/rac/plugin/rac_plugin_entry.h`) when `create` is a plain forward\n   onto `rac_<primitive>_<name>_create` — sherpa uses it for STT/TTS/VAD. Engines\n   with richer create flows (llamacpp, onnx, neurt, qhexrt) hand-write it.\n4. **`capability_check`** when the engine is platform- or binary-gated: use the\n   shared 3-way helper `rac_engine_unavailable_capability(platform_supported,\n   backend_present)` from `engines/common/rac_engine_unavailable.h`. When the\n   backend can be entirely absent at build time (a private prebuilt archive, a\n   sibling private repo), emit the stub with `RAC_ENGINE_UNAVAILABLE_PLUGIN(name,\n   display, cap_fn)` from the same header — see qhexrt and neurt for the two-mode\n   (ROUTABLE vs STUB) pattern this produces.\n5. **Registration carriers:** `RAC_PLUGIN_ENTRY_DEF(<name>)` returning the vtable; a\n   `rac_backend_<name>_register()`/`_unregister()` pair if you have extra bring-up\n   (called directly by SDK bridges on dynamic-link hosts); a\n   `rac_static_register_<name>.cpp` shim using `RAC_STATIC_REGISTER_BACKEND(<name>)`\n   (routes through the register fn) or `RAC_STATIC_PLUGIN_REGISTER(<name>)` (calls\n   the entry directly) for iOS/WASM static hosts.\n6. **CMake:** call `rac_add_engine_plugin(<name> SOURCES … LINK_LIBRARIES …\n   AVAILABILITY … PACKAGE_OWNER … PACKAGE_NAME …)` (`cmake/plugins.cmake`) from\n   `engines/<name>/CMakeLists.txt`, fronted by your own\n   `option(RAC_BACKEND_<NAME> \"…\" <default>)` + `if(NOT RAC_BACKEND_<NAME>)\n   return() endif()` self-gate (each engine owns its own option — `engines/CMakeLists.txt`\n   just unconditionally `add_subdirectory()`s and lets the child decide). Add\n   `add_subdirectory(<name>)` there. Declare `primitives`/`runtimes`/`formats` ONLY\n   in the C manifest, never in CMake, so the build graph can't drift from what the\n   router routes.\n7. **Android:** if the SDK loads the engine via `System.loadLibrary` +\n   `nativeRegister`, add a JNI bridge with `RAC_DEFINE_ENGINE_JNI_BRIDGE(...)`\n   (standalone per-engine `.so` — onnx, llamacpp) or\n   `RAC_DEFINE_ENGINE_JNI_BRIDGE_NO_ONLOAD(...)` (folded into a host lib that\n   already owns `JNI_OnLoad`, e.g. cloud → `librunanywhere_jni.so`) from\n   `engines/common/rac_engine_jni_bridge.h`. The JVM class-path token in the macro\n   call must match the Kotlin `*Bridge` class byte-for-byte.\n8. **iOS/WASM static hosts:** make sure the host force-loads the plugin —\n   `rac_force_load(<app> PLUGINS <name>)` in the host's CMakeLists — so the\n   static-init Registrar TU survives linker stripping.\n9. **If this engine ships an Electron/npm carrier package**, register it in\n   `scripts/validation/gates/check_plugin_natives.py`'s `REQUIRED_OPS` dict (the\n   ops-table symbol name(s) your entry TU references only on the routable\n   branch) — `package-sdk.sh` calls this automatically, so an unregistered\n   engine's stub/shell builds ship undetected. **Check which FILE actually\n   compiles `rac_plugin_entry_<name>.cpp`** before assuming the carrier\n   (`runanywhere_<name>`) is what to gate: sherpa/onnx/llamacpp compile it into\n   the carrier itself (an undefined symbol reference there is the evidence),\n   but neurt's `rac_add_engine_plugin()` call puts it on the ENGINE target\n   (`rac_backend_neurt`) instead — the carrier there is empty boilerplate with\n   zero symbols. Add the engine id to `EVIDENCE_IN_BACKEND_FILE` if it follows\n   neurt's shape; discovered by actually building neurt for real and running\n   `nm -a` on both files, not by reading the CMakeLists and assuming.\n\n## Procedure B — add a modality to an existing engine\n\nMulti-modality is the whole point of an identity-named engine. `cloud` and `neurt`\nare explicitly built for this and carry the recipe inline as a comment next to\ntheir vtable.\n\n1. Implement the new modality's op-table (e.g. a cloud TTS `g_cloud_tts_ops`,\n   typically backed by a per-modality provider adapter under `providers/`).\n2. **Fill the corresponding slot** in the engine's `rac_engine_vtable_t`\n   (`tts_ops` / `llm_ops` / `embedding_ops` / …) — every other slot must stay\n   explicit NULL.\n3. **Add the primitive** to the manifest's `primitives[]`.\n4. Update `formats[]`/`runtimes[]` only if the new modality actually needs them\n   (per THE RULE — don't declare a runtime your new op-table doesn't depend on).\n\nNo new plugin, no rename, no ABI bump — the engine already owns one\n`rac_engine_vtable_t` with all ten primitive slots; you're just filling another one.\nGrep the target engine's `rac_plugin_entry_<name>.cpp` for the phrase \"To add a\"\n— cloud and neurt both leave a comment naming exactly which slots are still free\nand what to add to `primitives[]`.\n",".agents/skills/sdk-release/SKILL.md":"---\nname: sdk-release\ndescription: Cut a new RunAnywhere SDK release end-to-end in RunanywhereAI/runanywhere-sdks — version bump through a fully green, tagged, drafted-then-published GitHub Release. Stops BEFORE publishing to package registries (npm/Maven/pub.dev/SwiftPM dist) — that is the separate \"sdk-publish\" skill, invoked as the last step here.\n---\n\n# SDK Release (runanywhere-sdks)\n\nRepo: `~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks` (verify with `pwd`/`ls` —\nthis is a workspace subfolder, not the repo root of the whole Qualcomm/ tree, and paths\ndrift; re-check script paths below still exist before trusting them blindly).\n\nThis is a **runbook**, not a description. Follow the steps in order. Every `gh`/`git`\ncommand below is meant to be run close to verbatim — adjust only the version/PR/run-id\nvalues for the release you're actually cutting.\n\n**THIS SKILL HAS BEEN SKIPPED THREE TIMES IN A ROW (v0.20.29, v0.20.30, v0.20.31),\nproducing the IDENTICAL failure each time, DESPITE step 2/3 below documenting the exact\nfix since the v0.20.25/v0.20.29 incidents.** All three times, an agent ran\n`sync-versions.sh`, opened the PR, merged it, and pushed the tag directly — skipping the\n\"dispatch a release candidate BEFORE merging, sync checksums from it, THEN merge\" sequence\nin steps 2-3 — usually because the version bump was one step inside a longer autonomous\nchain (a downstream repo re-pin, a multi-repo release cascade) and felt like \"just a\nversion bump,\" not a release. **A version bump to `core/VERSION` IS a release. If you are\nabout to run `scripts/release/sync-versions.sh` for ANY reason — including re-pinning this\nrepo as a step in some OTHER repo's release — stop and follow this skill from step 1,\nincluding the candidate dispatch. Do not improvise the sequence from memory even if you\ndid it correctly last time; do not skip it because \"this bump is small\" or \"we're in a\nhurry.\"** v0.20.31 additionally caught a second, previously-undetected instance of the\nsame class of bug in the SAME commit: `RunAnywhereMLXRuntime` in the Flutter podspec (which\n`RAC_CHECKSUM_SKIP` exempts from CI verification, so it fails silently for a consumer\ninstead of failing `publish`) was ALSO stale, and had to be independently re-verified\nagainst the same candidate build — see step 3's checksum-location table; check every row\nin it, not just the one CI complained about.\n\nCompanion skill: **sdk-publish** — invoke it at the very end (step 10) for the actual\nnpm / Maven / pub.dev / SwiftPM-distribution publishing. This skill's job stops at a\npublished (non-draft) GitHub Release with correct assets.\n\n---\n\n## 0. Orientation facts (confirm these still hold before relying on them)\n\n- Canonical version file: `core/VERSION` (single line, e.g. `0.20.24`). `core/VERSIONS`\n  also carries a `PROJECT_VERSION=` line that must agree — `sync-versions.sh` updates\n  both, `auto-tag.yml` reads `core/VERSIONS`, `check_release_version_coherence.sh` reads\n  `core/VERSION`. Don't hand-edit either; always go through the script.\n- `scripts/release/sync-versions.sh <version>` — bumps ~20 files (Package.swift,\n  gradle.properties, all package.json/pubspec.yaml, Kotlin/Swift/TS version constants,\n  AGENTS.md). Accepts a leading `v` and strips it.\n- `scripts/validation/gates/check_release_version_coherence.sh` — the gate that enforces\n  \"PR changing VERSION must carry exactly one `release:patch|minor|major` label, and the\n  new version must be exactly base+1 for that bump type.\" Reads `PR_BASE_SHA` and\n  `PR_RELEASE_LABELS_JSON` env vars when run under CI; locally it only checks the\n  canonical-version-format part.\n- `.github/workflows/pr-build.yml` job **`centralization`** is what actually runs the\n  coherence gate in CI (`Release-train version coherence` step), alongside\n  `check_typescript_centralization.sh`, `check_flutter_centralization.sh`,\n  `check_gradle_centralization.sh`, `check_wasm_provenance_contract.sh`,\n  `check_swift_dist_repo_sync.sh`, `check_agents_claude_sync.sh`,\n  `check_no_hardcoded_defaults.sh`. This job's trigger types are\n  `[opened, synchronize, reopened, labeled, unlabeled]` — **adding the release label\n  after opening the PR fires a fresh run**, which is how the gate clears without a new\n  commit (see step 1).\n- `.github/workflows/release.yml` — the build/package/draft-release workflow. Triggers:\n  tag push `v*.*.*` OR `workflow_dispatch` (works on **any branch**, not just tags) with\n  inputs `version` (required), `validate_external_starters` (bool, default false),\n  `publish_from_run_id` (optional), `reuse_native_web_run_id` (optional).\n- `.github/workflows/auto-tag.yml` — fires on `pull_request: closed` to `main` when the\n  merged PR carries a `release:*` label. Verifies the version bump matches the label,\n  pushes the `v<version>` tag, and **explicitly dispatches a fresh `release.yml` run at\n  that tag** (plain `git push` of a tag with `GITHUB_TOKEN` does not auto-trigger\n  workflows, so it calls `gh workflow run release.yml --ref v<version> -f version=<version>`\n  itself). This is the redundant rebuild you must cancel — see step 7.\n- Branch protection on `main`: check\n  `gh api repos/RunanywhereAI/runanywhere-sdks/branches/main/protection` — 2 approvals +\n  code owner review required, but look for a `bypass_pull_request_allowances` (users/teams/apps)\n  list. If your `gh api user --jq .login` is on that list, `gh pr merge --squash --admin`\n  is a legitimate, pre-configured bypass — not a hack. If not on the list, stop and ask a\n  human to approve/merge.\n- Never let any `qhexrt`-named asset reach the public asset set. `release.yml`'s own\n  manifest-assertion step (`find release-flat -maxdepth 1 -type f -iname '*qhexrt*'`)\n  hard-fails the run if one is found — this is enforced, not just policy.\n- **Electron IS part of `release.yml` now** (as of `fix/electron-cicd-staging-url-npu-ane`,\n  2026-08-21) — this reverses the prior \"deliberately separate\" policy noted below by an\n  earlier pass; the repo owner explicitly asked for Electron to produce ready-to-publish\n  artifacts through the real release train like every other SDK. `native_electron`\n  invokes `.github/workflows/electron-native-package.yml` as a **reusable workflow**\n  (`workflow_call`) rather than duplicating its ~400 lines of hard-won native-build logic\n  inline; `sdk_electron` downloads the resulting `electron-packaged-tarballs` artifact,\n  runs `scripts/release/prepublish_check.py` on every tarball, computes checksums, and\n  splits public vs. QHexRT-private before anything reaches `release-flat`. `publish`\n  hard-gates on `sdk_electron` succeeding (same tier as `sdk_kotlin`/`sdk_web`/\n  `sdk_react_native`) but treats `native_electron`'s own result as advisory, because that\n  job's result folds in `native_win_arm64_qhexrt` — a single, personally-owned\n  self-hosted NPU runner whose outage should cost only `@runanywhere/electron-qhexrt`\n  for that release, not the whole SDK train (`native_win_x64_and_package`, the job that\n  actually produces the tarballs, tolerates a failed/skipped QHexRT lane and still packages\n  the other five real artifacts). Like every other SDK here, this still does **not**\n  `npm publish` anything — it only produces the tarballs as GitHub Release assets\n  (public ones) / a workflow artifact (the private QHexRT one) for a human to publish by\n  hand, matching the \"build automated, publish manual\" shape everywhere else.\n  **Electron is no longer WIP on a branch** — the old root `AGENTS.md`/`CLAUDE.md`\n  \"Active issues\" section describing `smonga/electron_upgrade` as in-progress is gone\n  entirely as of this checkout; `bindings/electron/` is a fully merged, real SDK on\n  `main` with its own `AGENTS.md`. Re-check that section is still absent before trusting\n  this note — if it has reappeared, something regressed.\n  See `thoughts/shared/plans/electron_cicd_staging_url_and_npu_ane_pipeline.md` for the\n  full history (this pipeline shipped the placeholder-URL bug three times before\n  `electron-native-package.yml` existed).\n  **QHexRT public/private boundary — CONFIRMED POLICY, not an open flag**: the repo\n  owner explicitly confirmed (2026-08-20) that `@runanywhere/electron-qhexrt` stays\n  public on npm intentionally (it already is, and is a real, working precedent worth\n  studying before touching QHexRT packaging elsewhere) — the QHexRT-stays-private\n  policy enforced everywhere else in this pipeline is specifically about the\n  **GitHub-Release-side** asset set (e.g. Android), a different distribution channel.\n  This is settled; stop re-flagging it as an unresolved contradiction. Only re-raise it\n  if you find evidence the policy has changed.\n\n---\n\n## 0b. NeuRT is a PINNED PREBUILT now — check this before anything else\n\n`native_ios` no longer compiles the private `neurun` repo. It downloads archives that\nrepo publishes, pinned by tag + SHA-256 in `core/VERSIONS`, the same shape as sherpa-onnx:\n\n```\nNEURUN_REPO=RunanywhereAI/neurun      # shared: one repo publishes BOTH engines\nNEURT_RELEASE_TAG=v<version>          # per-engine: a release may carry one lane only\nNEURT_RAC_ABI_VERSION=9\nNEURT_MACOS_ARM64_SHA256=…\nNEURT_IOS_ARM64_SHA256=…\nNEURT_IOS_ARM64_SIMULATOR_SHA256=…\n```\n\n`NEURUN_REPO` is shared because one neurun release carries both engines. The **tags stay\nper-engine** on purpose: that repo can cut a release for one lane only, so a given tag may\ncarry just one engine's assets.\n\n**Run the gate after re-pinning** — it verifies the pins against the release itself, so a\nmistyped or locally-computed value fails there instead of mid-build:\n\n```bash\nNEURUN_TOKEN=\"$(gh auth token)\" bash scripts/validation/gates/check_engine_prebuilt_pins.sh\n```\n\n### Local engine development — there is no source mode\n\nThe engine has no `NEURT_ROOT` path any more; nothing here compiles engine source. To iterate\non NeuRT, build a slice in the neurun checkout and point the engine at it — no release, no\ntag, no pin edit:\n\n```bash\n# in neurun\nNeuRT/tools/scripts/package-rac-dist.sh --sdk-root <this repo> --version 0.0.0-dev \\\n    --slice macos-arm64 --out /tmp/devdist\nmkdir -p /tmp/devslice && tar -xzf /tmp/devdist/neurt-macos-arm64-v0.0.0-dev.tar.gz \\\n    -C /tmp/devslice --strip-components=1\n\n# here\nNEURT_PREBUILT_ROOT=/tmp/devslice cmake -S . -B build/dev -G Ninja \\\n    -DRAC_BUILD_BACKENDS=ON -DRAC_BACKEND_NEURT=ON -DRAC_RUNTIME_COREML=ON\n# expect: Mode: PREBUILT (macos-arm64, ABI 9)\n```\n\n`NEURT_PREBUILT_ROOT` is a **single-slice** root, which is why\n`build-core-xcframework.sh` deliberately ignores it: packaging needs all three slices, so\nhonouring a single-slice override there would let one staged slice satisfy the guard and\nsilently package two non-routable stubs. Use an obviously-not-a-release version like\n`0.0.0-dev` so a dev slice can never be mistaken for a published one.\n\n**Why this changed, and why it matters to you specifically.** `engines/neurt/CMakeLists.txt`\nused to resolve a `NEURT_ROOT` checkout of that private repo and **regex-parse its root\n`CMakeLists.txt`** for a source list. Under that arrangement, commons widened\n`rac_llm_stream_callback_fn` with `tokens_in_delta`, the pinned neurun commit still passed\n4 arguments, and `native_ios` died with *\"too few arguments to function call, expected 5,\nhave 4\"* on **v0.20.15, v0.20.16 and v0.20.17**. Every packaging job needs `native_ios`,\nso all three shipped **iOS-only — 18 assets where a healthy release has 55+**. That is the\nsingle most expensive failure this runbook exists to prevent, and it came from this seam.\n\n**Before opening the release PR**, confirm the pin is current:\n\n```bash\ngrep -A5 '^NEURT_REPO' core/VERSIONS\ngh release view \"$(grep '^NEURT_RELEASE_TAG=' core/VERSIONS | cut -d= -f2)\" \\\n    --repo RunanywhereAI/neurun --json assets --jq '.assets[].name'\n```\n\nExpect six assets — three `neurt-<slice>-v<version>.tar.gz` plus their `.sha256` sidecars.\n\n**If the plugin ABI changed this cycle** (`RAC_PLUGIN_API_VERSION` in\n`core/include/rac/plugin/rac_plugin_entry.h`), the pinned archives are invalid and\n`native_ios` will fail at the download step. Cut a neurun release first — that repo's\n`release_sdk_archives` skill is the runbook — then re-pin both `NEURT_RELEASE_TAG` and\n`NEURT_RAC_ABI_VERSION` here. The downloader refuses to run if `NEURT_RAC_ABI_VERSION`\ndisagrees with the header, so this cannot be half-done.\n\nProve it locally before trusting a tag build:\n\n```bash\nNEURUN_TOKEN=\"$(gh auth token)\" bash scripts/build/download-neurt.sh --all\n```\n\nThat verifies each tarball's SHA-256 **and** that its `RECEIPT.json` names the same ABI\nversion this repo defines. Then check configure prints `Mode: PREBUILT (<slice>, ABI N)`.\n`SOURCE` means a local neurun checkout was picked up (prebuilt is meant to win, so the\ndownload did not land); `SHELL` means the engine is **not routable** and a release built\nnow would ship an engine that registers, claims Apple availability and serves nothing.\n\n`release.yml` treats the downloader's exit **3** (\"no token — degrade to the shell\") as\n**fatal**. That degrade is right for a fork PR and catastrophic for a release.\n\n### The complete engine matrix — who consumes what, and how\n\nThere are only **three fetch points in the entire repo**. Everything else consumes an\nsdks artifact, which is why a single fetch fix can repair five SDKs at once — and why a\nsingle missing fetch can silently break several.\n\n```\nneurun release (private)\n  ├─ native_ios         ──NeuRT x3 slices──►  RABackendNeuRT.xcframework\n  │                                              ├─ Swift  (Package.swift binary target)\n  │                                              ├─ Flutter / React Native (co-vendored\n  │                                              │   in the onnx package's podspec)\n  │                                              └─ rcli-macos (stages it, then GREPS\n  │                                                  `backends --json` for neurt)\n  ├─ native_android     ──QHexRT arm64-v8a──►  one build stages jniLibs into\n  │                                              Kotlin + React Native + Flutter\n  └─ electron           ──NeuRT macos-arm64 AND QHexRT win-arm64──►  its own natives\n```\n\n| target | NeuRT | QHexRT | note |\n|---|---|---|---|\n| Swift (iOS/macOS) | ✅ xcframework | — | NeuRT is Apple-only |\n| Flutter / React Native (Apple) | ✅ xcframework | — | co-vendored in the onnx package |\n| Flutter / React Native (Android) | — | ✅ jniLibs | from the same Android build as Kotlin |\n| Kotlin (Android) | — | ✅ jniLibs | arm64-v8a only; the only ABI with a Hexagon NPU |\n| Electron macOS | ✅ fetches | — | builds its own natives |\n| Electron win-arm64 | — | ✅ fetches | |\n| rcli macOS | ✅ xcframework | — | **best check in the tree** (below) |\n| rcli linux / windows-x64 | — | — | correct: NeuRT is Apple-only, QHexRT is win-arm64 |\n\n**`rcli-macos` is the strongest verification that exists here.** It greps\n`backends --json` for `\"name\":\"neurt\"` — and a non-routable shell *refuses\nregistration*, so that grep proves the engine is **routable**, not merely present. Copy\nthis pattern when adding an engine rather than checking for a file on disk.\n\n**Two silent-failure traps this matrix exists to prevent:**\n\n- **`RAC_BACKEND_<X>=ON` does not mean the engine works.** Without a payload the engine\n  builds as a non-routable shell and registration is refused — no build error. Android\n  shipped that way for months, and `rcli-macos-release` still sets\n  `RAC_BACKEND_NEURT: ON` regardless of whether a payload was fetched.\n- **`copy_if_exists` in `build-core-android.sh` silently no-ops** when a lib was never\n  built. That is the same guarded-copy pattern that shipped Windows with zero engines for\n  several releases. The release now asserts `librunanywhere_qhexrt.so` is really inside\n  the arm64-v8a archive.\n\n`scripts/validation/gates/check_engine_prebuilt_pins.sh` asserts all of the above plus\nthe pins, the ABI, header drift against the receipt, and (with a token) the release\ncontents. Run it before cutting a release; it is also wired into `pr-build` offline.\n\n### QHexRT is pinned the same way — check it too\n\nThe Hexagon-NPU engine crosses the identical boundary, published by the same neurun release:\n\n```\nQHEXRT_RELEASE_TAG=v<version>\nQHEXRT_ARM64_V8A_SHA256=…      QHEXRT_ARM64_V8A_RECEIPT=…\nQHEXRT_WIN_ARM64_SHA256=…      QHEXRT_WIN_ARM64_RECEIPT=…\n```\n\nTwo pins per ABI, not one: the tarball checksum **and** the payload's own build-receipt\nhash. The receipt is the directory name under `engines/qhexrt/prebuilt/versions/`, so\npinning it is what makes the pin describe the bytes rather than trusting the archive's\nown claim about itself.\n\n```bash\nNEURUN_TOKEN=\"$(gh auth token)\" bash scripts/build/download-qhexrt.sh --abi arm64-v8a\nNEURUN_TOKEN=\"$(gh auth token)\" bash scripts/build/download-qhexrt.sh --abi win-arm64\n```\n\n**`current` selects ONE ABI at a time.** Both payloads can sit in `versions/` together, but\na build targets one platform — so re-run the downloader when switching between an Android\nand a Windows build. The downloader ends by running `validate-qhexrt-prebuilt.py` itself,\nso a bad payload fails at the pin rather than mid-build.\n\n**The QHexRT lane in neurun is deliberately non-blocking.** It runs on a single\nself-hosted Snapdragon box (QAIRT is licence-gated and exists on no hosted runner). If\nthat machine is offline, neurun's release publishes the Apple artifacts and **warns** that\nno QHexRT payload was produced — and the SDK simply keeps its previously pinned prebuilt.\nThat is correct behaviour, not a failure: do not block an SDK release on it. Only treat it\nas blocking if this release is specifically meant to ship new NPU kernels.\n\n**Do not hand-stage `engines/qhexrt/prebuilt/`.** That was the old flow, and it is how the\nElectron win-arm64 lane once pinned a receipt staged months earlier and silently shipped\nNPU kernels from an old commit. If the tree looks stale, re-pin and re-download; never\ncopy a payload in by hand.\n\n---\n\n## 1. Decide the version and open the release PR\n\nCheck for an already-open release PR first — don't duplicate work:\n\n```bash\ngh pr list --repo RunanywhereAI/runanywhere-sdks --label release:patch --label release:minor\n```\n\n(gh ORs multiple `--label` flags into a single AND-filtered query per invocation is not\nguaranteed across all gh versions — if unsure, run the two label queries separately:\n`--label release:patch` then `--label release:minor`, plus `--label release:major`.)\n\nPick a patch/minor/major bump based on the changes since the last tag. Then:\n\n```bash\ngit checkout -b release/v<version>\n./scripts/release/sync-versions.sh <version>      # e.g. 0.20.25 (no 'v' needed, either works)\nbash scripts/validation/gates/check_release_version_coherence.sh\n```\n\nThe coherence script may point out generated locks/changelogs that need regenerating\n(e.g. package-lock.json version bumps it expects but didn't find, or a missing CHANGELOG\nheading for the new version). Fix everything it flags, and add **explicit, human-written\nchangelog entries** — the gate requires a heading for the new version to exist; it does\nnot write prose for you.\n\nOpen the PR and **apply exactly one `release:patch` or `release:minor` (or `release:major`)\nlabel** — this is required, not optional. Two failure modes to know:\n\n- If you forget the label and push a commit later, the `centralization` job's\n  `Release-train version coherence` step fails.\n- **Adding the label after the PR is already open does NOT get picked up by the run that\n  already executed.** Because `pr-build.yml`'s `centralization` job triggers on\n  `labeled`/`unlabeled` PR events, applying the label fires a **new** run of the whole\n  job — that new run is what goes green. Don't push an empty commit to \"retrigger\" it;\n  just add/re-add the label (or use `gh pr edit <pr> --add-label release:patch`, and if it\n  was already present, remove + re-add to force the event).\n\n---\n\n## 2. Get REAL native-artifact checksums BEFORE merging — dispatch a release candidate\n\nDo not merge on faith that native builds will succeed. `release.yml`'s\n`workflow_dispatch` trigger works on **any branch**, including your unmerged release\nbranch — use it to build a full release candidate first:\n\n```bash\ngh workflow run release.yml --ref release/v<version> -f version=<version>\n```\n\nGet the run id and watch it:\n\n```bash\ngh run list --repo RunanywhereAI/runanywhere-sdks --workflow=release.yml --limit 5\n```\n\n**Do not block synchronously waiting on it.** Load the `Monitor` tool (or a background\npoll loop) to watch for completion/failure, and keep doing other release prep (changelog\nreview, CodeRabbit thread triage, branch-protection check) in the meantime. Keep\nnotifications to real state changes only — don't ping on every intermediate job\ncompleting, only on the run's overall failure or full completion.\n\n### REAL job durations — measured, not guessed. NEVER cancel a job for being \"slow\".\n\nMeasured on the 0.20.25 train (2026-08-21/22). A release train legitimately takes\n**4-5 hours** wall-clock when `native_web` has to build.\n\n| Job | Real duration | Notes |\n|---|---|---|\n| **`native_web`** (release.yml) | **~3h51m** | The long pole, by far. Its `Vendor ONNX Runtime and Sherpa-ONNX WASM archives` step alone is **~71 minutes** — it BUILDS onnxruntime + sherpa to WASM from source; it is not a git clone. Then 4 more WASM builds: CPU core+llamacpp+onnx ~89m, WebGPU llamacpp ~37m, WebGPU onnx+sherpa ~33m. |\n| `native_ios` | ~60-75 min | XCFrameworks + MLX. No longer compiles neurun — it downloads the pinned NeuRT archives (see §0b), which is seconds, not minutes. |\n| `swift-spm` (pr-build) | **69-85 min** | `Build XCFramework` step is most of it. Measured across 6 consecutive `main` runs (2026-08-21/22): 69, 78, 75, 85, 77, 69. A run sitting at ~78 minutes is **healthy**, not hung — check this row before reaching for cancel. |\n| `kotlin-android` (pr-build) | ~40 min | |\n| `python-windows` (pr-build) | ~40 min | |\n| `electron (windows-2022)` (electron-sdk-ci) | ~40 min | |\n| `rcli-windows` | ~30 min | |\n| `native_electron / native_win_arm64_qhexrt` | ~28 min | Self-hosted; may sit **queued** for a long time first if the single runner is busy. Queued ≠ hung. |\n| `native_electron / native_win_x64_and_package` | ~35-45 min | |\n| `wasm` (pr-build) | ~12 min | **Do NOT use this as a proxy for `native_web`.** Totally different scope. |\n\n**The mistake this table exists to prevent**: `pr-build.yml`'s `wasm` job takes ~12\nminutes, so a ~50-minute `native_web` \"vendor\" step looks hung. It is not. On the 0.20.25\ntrain this misread caused two healthy `native_web` jobs to be cancelled at 2h19m and\n~55m, wasting ~3 hours and two full candidate builds. Before concluding \"hung\", check\nstep-level progress and compare against THIS table:\n\n```bash\ngh run view <run-id> --repo RunanywhereAI/runanywhere-sdks --json jobs \\\n    --jq '.jobs[] | select(.name==\"native_web\") | {startedAt, steps: [.steps[] | select(.status==\"in_progress\") | .name]}'\n```\n\n### `reuse_native_web_run_id` is the correct way to avoid re-paying that ~4 hours\n\nIf a run needs redoing and **nothing under `bindings/web/wasm/`, `engines/`, or `core/`\nchanged** since a run whose `native_web` succeeded, reuse it instead of rebuilding:\n\n```bash\ngit diff --stat <good-run-sha> <current-sha>      # prove the scope first\ngh workflow run release.yml --ref release/<version> \\\n    -f version=<version> -f reuse_native_web_run_id=<good-run-id>\n```\n\n`native_web` then shows `skipped`, and `sdk_web` + `validate_consumer_web` still run and\npass against the reused artifact — which is also your proof the reuse was valid. Track the\nrun id that **originally built** it: that is the id you keep citing (see the chaining note\nin step 7).\n\n---\n\n## 3. Sync checksums once native_ios finishes\n\nPre-tag, `gh release download` will not work (there is no release yet) — pull the run's\nartifact directly:\n\n```bash\ngh run download <candidate-run-id> --repo RunanywhereAI/runanywhere-sdks \\\n    --name native-ios-macos --dir release-artifacts/native-ios-macos\n```\n\nVerify every `.zip` against its `.sha256` sidecar **yourself** before trusting it:\n\n```bash\ncd release-artifacts/native-ios-macos\nfor f in *.zip; do shasum -a 256 -c \"${f}.sha256\" || echo \"MISMATCH: $f\"; done\ncd -\n```\n\nThen sync:\n\n```bash\nbindings/swift/scripts/sync-checksums.sh release-artifacts/native-ios-macos\n```\n\n**KNOWN FACT — do not treat this as a bug**: `RunAnywhereMLXRuntime` (and occasionally\n`RACommons`) are **not byte-reproducible** between rebuilds of identical source. Expect\ntheir checksums to differ from any prior candidate build every single time you rebuild,\neven with zero source changes. Only investigate if a checksum for a framework that\nhistorically IS reproducible (e.g. plain XCFrameworks with no Swift codegen step)\nsuddenly changes.\n\n**Where each checksum actually lives — these are NOT all in one file:**\n\n| Manifest | Carries |\n|---|---|\n| root `Package.swift` | `RACommons` + the 5 backend XCFrameworks (6 checksums total) |\n| nested Flutter `Package.swift` (under `bindings/flutter/`) | same 6 |\n| `bindings/flutter/packages/runanywhere_mlx/ios/runanywhere_mlx.podspec` → `mlx_checksums` hash | `RunAnywhereMLXRuntime`, `RunAnywhereMLXMetal`, `RunAnywhereMLXResources` — **only here**, nowhere else |\n\nAfter `sync-checksums.sh` runs, verify centralization didn't drift and re-run the version\ncoherence gate locally:\n\n```bash\nbash scripts/validation/gates/check_flutter_centralization.sh\nbash scripts/validation/gates/check_release_version_coherence.sh\n```\n\nCommit and push the checksum-sync commit to the release branch:\n\n```bash\ngit add Package.swift bindings/flutter/Package.swift \\\n    bindings/flutter/packages/runanywhere_mlx/ios/runanywhere_mlx.podspec\ngit commit -m \"release: sync v<version> checksums from candidate run <candidate-run-id>\"\ngit push\n```\n\n### THIS STEP IS NOT OPTIONAL — skipping it fails `publish` after the tag already exists\n\n**What happens if you skip it** (observed on the 0.20.25 train): every build and package\njob goes green, `publish` gets all the way past flatten and the asset-manifest assertion,\nthen dies at **`Verify built Apple checksums match tagged package contracts`**:\n\n```\nmismatch: RACommonsBinary\n  Package.swift: 0609dae6...      release zip: 1ae55ae4...\nmismatch: RABackendLlamaCPPBinary\n  Package.swift: 0597442f...      release zip: 7062f7c8...\n>> Done. 8 processed, 0 missing, 4 failed.\nERROR: built Swift archives do not match the immutable tagged manifest\n```\n\nNote `8 processed / 4 failed` is really **2 distinct archives counted twice** (root\nmanifest + Flutter manifest each). On that train `RACommons` and `RABackendLLAMACPP`\ndrifted while ONNX/Sherpa/NeuRT/MLX/MLXMetal/MLXResources all matched — so \"6 of 8 passed\"\nis a normal-looking partial failure, not evidence of something exotic.\n\n**Recovery once the tag already exists** (this is safe and was done on 0.20.25):\n\n1. Confirm nothing was actually published — a failed `publish` dies *before* creating the\n   release, so there is usually nothing to undo:\n   ```bash\n   gh release view v<version> --repo RunanywhereAI/runanywhere-sdks   # expect \"release not found\"\n   ```\n2. Download the **exact artifacts the release will publish** (the candidate run named in\n   `publish_from_run_id`), verify each against its sidecar, and sync on a branch off `main`.\n3. Merge that branch, then **re-point the tag** at the new commit. `git push origin --delete`\n   can hang on flaky networks; the API is more reliable:\n   ```bash\n   gh api -X DELETE repos/RunanywhereAI/runanywhere-sdks/git/refs/tags/v<version>\n   gh api -X POST   repos/RunanywhereAI/runanywhere-sdks/git/refs \\\n       -f ref=\"refs/tags/v<version>\" -f sha=\"<new-main-sha>\"\n   ```\n   Only do this while **no release exists and nothing has been published**. If a release\n   was already published, do NOT move the tag — cut the next patch version instead.\n4. **Creating a tag via the API DOES trigger `release.yml`'s `push` trigger** (unlike a\n   `GITHUB_TOKEN` git-push of a tag, which does not). So immediately after re-tagging you\n   will have TWO runs: a full rebuild from the `push` event, and your own\n   `workflow_dispatch`. Identify by event and cancel the `push` one:\n   ```bash\n   gh run list --repo RunanywhereAI/runanywhere-sdks --workflow=release.yml --limit 3 \\\n       --json databaseId,event,headBranch --jq '.[] | \"\\(.databaseId) \\(.event) \\(.headBranch)\"'\n   gh api -X POST repos/RunanywhereAI/runanywhere-sdks/actions/runs/<push-run-id>/cancel\n   ```\n\nCheapest prevention: run `sync-checksums.sh --check <zip_dir>` against the candidate's\nartifacts and require **`N processed, 0 missing, 0 failed`** before you merge the release\nPR at all.\n\n**Recurred on v0.20.30 and again on v0.20.31** — same `mismatch: RACommonsBinary` /\n`mismatch: RACommons` failure, same root cause (step 2's candidate dispatch skipped\nentirely both times), fixed the same way both times: compute the real checksum from the\nalready-built release-artifact zip (`gh run download <failed-run-id> --name\nnative-ios-macos`), commit it on a fix branch, merge, then move the tag (§10d) to\nre-trigger `release.yml`. On v0.20.31 this ALSO surfaced the `RunAnywhereMLXRuntime`\npodspec entry as independently stale — `RAC_CHECKSUM_SKIP` means `publish` never checks\nit, so it would have shipped broken to Flutter consumers silently. **When fixing this\nfailure after the fact, check every row of the checksum-location table in step 3, not\nonly the archive named in the CI error** — the ones `RAC_CHECKSUM_SKIP` exempts are\nexactly the ones that won't tell you.\n\n---\n\n## 4. Watch for the \"package intentionally doesn't bundle a sibling dep\" bug class\n\nThis exact release hit a real bug in `bindings/web/scripts/package-sdk.sh`:\n`@runanywhere/web-onnx` deliberately does **not** bundle its own `proto-ts` copy (only\n`core`/`llamacpp` do — enforced by `bindings/web/scripts/validate_public_packages.py` and\nmirrored in `bindings/react-native/scripts/validate_public_packages.py`), but the\npackaging script's install-smoke-test (`verify_candidate_install`, around line 165 of\n`package-sdk.sh`) never supplied a resolution target for that unbundled dependency, so\n`npm install` reached out to the real npm registry for a not-yet-published version and\nfailed with `ETARGET`.\n\nFix pattern (already applied, keep it this way): the `onnx` call to\n`verify_candidate_install` passes the just-built local `proto-ts` tarball\n(`$PROTO_ARCHIVE`, built via `npm pack ../proto-ts`) as an explicit extra install arg —\nsee lines ~209-233 of `bindings/web/scripts/package-sdk.sh`, where `core` and `llamacpp`\ncalls omit it (they bundle proto-ts themselves) but `onnx`'s call appends `\"$PROTO_ARCHIVE\"`.\n\n**Generalize this as a standing check for every release**: grep for the asymmetric\nbundling flags before trusting any packaging script —\n\n```bash\ngrep -n \"expects_bundled_proto\\|--bundle\" \\\n    bindings/web/scripts/package-sdk.sh bindings/react-native/scripts/package-sdk.sh\n```\n\nWhenever a package intentionally does NOT bundle a sibling first-party dependency, its\nlocal pre-publish verification step must supply that dependency some other way (a local\ntarball) — the real registry version genuinely does not exist yet at candidate-build\ntime, and letting `npm`/`yarn` hit the real registry will `ETARGET`.\n\n**Before trusting any packaging script is bug-free, actually run it** against the real\ndownloaded native artifacts and check the exit code — do not just read the code and\nassume it's fine:\n\n```bash\ncd bindings/web && ./scripts/package-sdk.sh   # then check $?\ncd bindings/react-native && ./scripts/package-sdk.sh\n```\n\n### 4b. The two Electron-specific release bugs (both fixed — know the shapes)\n\nThe 0.20.25 train was the **first** release to run Electron packaging inside `release.yml`,\nand it surfaced two defects that no local test or green PR check could have caught. Both\nare fixed; recognize the shapes because they generalize.\n\n**(1) `sync-versions.sh` has a hardcoded package list, and new packages get missed.**\n`bindings/electron/packages/neurt/package.json` sat at the previous version while every\nsibling moved, because `neurt` was added (electron#734) after that list was last touched.\nSymptom: `native_win_x64_and_package` fails with\n\n```\nERROR: npm pack did not produce .../runanywhere-electron-neurt-<version>.tgz\n```\n\n— because `npm pack` correctly produced `...-neurt-<OLD-version>.tgz` from the stale\nmanifest. **Whenever you add a package anywhere, add it to `sync-versions.sh` the same\nday.** Cheap pre-flight check that catches the whole class:\n\n```bash\ngrep -h '\"version\"' bindings/electron/package.json bindings/electron/packages/*/package.json \\\n    bindings/electron/native/package.json | sort -u     # expect exactly ONE distinct version\n```\n\n**(2) `publish` downloads EVERY artifact, including intermediate hand-off bundles.**\n`publish`'s `Download all artifacts` step has no name filter. `electron-packaged-tarballs`\nis `native_win_x64_and_package`'s raw output whose only purpose is feeding `sdk_electron`,\nwhich re-uploads the curated public set as `sdk-electron`. Flattening both duplicated all\n5 Electron tarballs *and* dragged in the raw bundle's own `proto-ts` copy, which collided\nwith `sdk_web`'s:\n\n```\n::error::flatten collision: release-flat/runanywhere-proto-ts-<v>.tgz already exists\n::error::Refusing to publish — 6 filename collision(s) detected during flatten\n```\n\nFixed by pruning that directory in the flatten `find`\n(`-type d -name \"electron-packaged-tarballs\" -prune -o -type f \\( ... \\)`). **Standing\nrule: any new intermediate artifact must be either excluded from the flatten or named so\nit cannot collide.** When editing that `find`, test the exact expression against a fake\n`release-artifacts/` tree locally first — `-prune -o` + `-print0` is easy to get subtly\nwrong and the only other way to find out costs a full release run.\n\n---\n\n## 5. Resolve CodeRabbit (or any bot) review findings properly\n\nDo not resolve a review thread without a real, evidence-backed reply. Steps:\n\n1. Fetch unresolved threads via GraphQL:\n\n```bash\ngh api graphql -f query='\nquery($owner:String!, $repo:String!, $pr:Int!) {\n  repository(owner:$owner, name:$repo) {\n    pullRequest(number:$pr) {\n      reviewThreads(first:100) {\n        nodes { id isResolved comments(first:10) { nodes { id databaseId body } } }\n      }\n    }\n  }\n}' -f owner=RunanywhereAI -f repo=runanywhere-sdks -F pr=<pr-number>\n```\n\n2. For each unresolved thread, reply via REST with `in_reply_to` set to the comment's\n   `databaseId` (not the GraphQL node id), citing concrete evidence — a real run id,\n   a real checksum, a real commit SHA, not \"looks fine\":\n\n```bash\ngh api -X POST repos/RunanywhereAI/runanywhere-sdks/pulls/<pr-number>/comments \\\n    -f body=\"Verified against candidate run <run-id>: RunAnywhereMLXRuntime checksum\nmatches release-artifacts/native-ios-macos/RunAnywhereMLXRuntime-ios-v<version>.zip.sha256.\nSee commit <sha>.\" \\\n    -F in_reply_to=<comment-database-id>\n```\n\n3. Only then resolve, via GraphQL mutation on the thread id from step 1:\n\n```bash\ngh api graphql -f query='\nmutation($threadId:ID!) { resolveReviewThread(input:{threadId:$threadId}) { thread { isResolved } } }\n' -f threadId=<thread-node-id>\n```\n\n---\n\n## 6. Merge\n\nBefore merging, verify:\n\n```bash\ngh pr view <pr-number> --repo RunanywhereAI/runanywhere-sdks --json mergeable,reviewDecision\n```\n\nZero unresolved review threads (re-check the GraphQL query from step 5 — it should return\nnone with `isResolved: false`), and `mergeable == \"MERGEABLE\"`.\n\nConfirm bypass eligibility before relying on `--admin`:\n\n```bash\ngh api repos/RunanywhereAI/runanywhere-sdks/branches/main/protection \\\n    --jq '.required_pull_request_reviews.bypass_pull_request_allowances'\ngh api user --jq .login\n```\n\nIf your login appears in that allowance list (users, or a team/app you belong to):\n\n```bash\ngh pr merge <pr-number> --repo RunanywhereAI/runanywhere-sdks --squash --admin\n```\n\nIf not on the list, **stop** — ask a human to approve or merge instead of forcing it.\n`--admin` is a legitimate configured bypass only for allow-listed identities; do not use\nit to route around review for anyone else.\n\n---\n\n## 7. GOTCHA — cancel the auto-tag-dispatched rebuild, dispatch a publish-only run instead\n\nThe moment the PR merges, `auto-tag.yml` runs, pushes the `v<version>` tag, and **itself**\ndispatches a brand-new full `release.yml` run at that tag (see orientation section — plain\ntag pushes via `GITHUB_TOKEN` don't auto-trigger, so the workflow does it explicitly).\nThis is redundant — you already have a fully green, checksum-verified candidate from step\n2-3 — and wasteful, because Apple/MLX bytes will drift again for zero benefit (see the\nnon-reproducibility fact in step 3).\n\n**Cancel it immediately, before it burns real compute** (ideally within the first minute\nor two, before native_ios starts real work):\n\n```bash\ngh run list --repo RunanywhereAI/runanywhere-sdks --workflow=release.yml --limit 5\ngh api -X POST repos/RunanywhereAI/runanywhere-sdks/actions/runs/<auto-triggered-run-id>/cancel\n```\n\nThen dispatch a **publish-only** run that reuses your already-verified candidate's exact\nbytes instead of rebuilding:\n\n```bash\ngh workflow run release.yml --ref v<version> \\\n    -f version=<version> \\\n    -f publish_from_run_id=<candidate-run-id>\n```\n\n**CRITICAL SUB-GOTCHA**: if your candidate run in step 2 itself passed\n`reuse_native_web_run_id=<X>` to skip rebuilding WASM (a packaging-only re-release), that\ncandidate run **never uploaded its own copy of the `native-web` artifact** — it was\nskipped. `publish_from_run_id` alone will then fail during the release-manifest assertion\nwith `\"Missing or empty Web WASM payload\"`, because the publish job's artifact-download\nstep only pulls `reuse_native_web_run_id`'s artifact when that input is passed to *this*\ndispatch too (see `.github/workflows/release.yml` around the \"Download reused Web natives\"\nstep — it is gated on `inputs.reuse_native_web_run_id != ''`, independent of\n`publish_from_run_id`). You must **also** pass the same original run id:\n\n```bash\ngh workflow run release.yml --ref v<version> \\\n    -f version=<version> \\\n    -f publish_from_run_id=<candidate-run-id> \\\n    -f reuse_native_web_run_id=<X>\n```\n\nThis chains forward across releases: whichever run **originally** built `native_web` is\nthe run you keep citing at every subsequent `reuse_native_web_run_id`, not the most recent\nrun that merely borrowed it from an earlier one. Track that original run id explicitly if\nyou're doing back-to-back packaging-only releases.\n\n---\n\n## 8. Verify the draft release's asset set\n\nThe publish job only creates a **draft** GitHub Release (`softprops/action-gh-release`,\n`draft: true`) with built assets attached — it does not touch npm/Maven/pub.dev at all\n(grep `release.yml` for `npm publish`, `mavenCentral`, `pub publish` — none exist there).\n\nExpected asset count for a full, non-Windows-focused release: **~65 assets** —\n\n- 9 Apple archives (iOS/macOS XCFrameworks: RACommons + 5 backends + MLX runtime/Metal/\n  resources + CoreML/NeuRT)\n- 3 Android RACommons zips (arm64-v8a, armeabi-v7a, x86_64)\n- 1 Linux RACommons tar.gz\n- 1 Windows RACommons zip (preserved as an asset, advisory — see step 9)\n- 1 Web WASM tar.gz (`RACommons-web-v<version>.tar.gz`)\n- 3 rcli tarballs (macOS arm64, Linux x86_64, Windows x86_64 — macOS may also have a\n  notarized `.dmg` if Developer ID/notary secrets are configured; this is soft-skipped\n  otherwise, not a failure)\n- 1 Kotlin Maven repo zip\n- 8 npm tarballs (proto-ts, web core, web-llamacpp, web-onnx, RN core, RN llamacpp, RN mlx,\n  RN onnx)\n- 5 Electron npm tarballs (`runanywhere-electron-<v>.tgz` core, `-llamacpp`, `-onnx`,\n  `-sherpa`, `-neurt`) — added by `sdk_electron`; each has its own `.sha256` sidecar\n  counted separately below. **`electron-qhexrt` is deliberately excluded** — it still\n  gets built and packaged, but only as the `sdk-electron-qhexrt-private` workflow\n  artifact (not a release asset); download it separately for a manual `npm publish`.\n- 1 `MANIFEST.txt`\n- **explicitly ZERO** `qhexrt`-named files (the manifest assertion step hard-fails the run\n  if it finds any — confirm the run actually reached the publish job and didn't die there)\n\nEvery asset above except `MANIFEST.txt` has a `.sha256` sidecar, which is itself a\nseparate asset. **v0.20.25 landed at exactly 65 assets** — use that as the concrete\nbaseline rather than re-deriving the arithmetic.\n\nCross-check every checksum's `.sha256` sidecar against what got committed to\n`Package.swift` / the flutter `Package.swift` / the MLX podspec in step 3 — they must\nmatch byte for byte, because this run's artifacts are exactly what step 10 (sdk-publish)\nships to real consumers.\n\n```bash\ngh run view <publish-run-id> --repo RunanywhereAI/runanywhere-sdks --json jobs \\\n    --jq '.jobs[] | {name, conclusion}'\n\n# Asset count + the Electron set + the qhexrt-leak assertion, in three commands:\ngh release view v<version> --repo RunanywhereAI/runanywhere-sdks --json assets --jq '.assets|length'\ngh release view v<version> --repo RunanywhereAI/runanywhere-sdks --json assets \\\n    --jq '[.assets[].name | select(startswith(\"runanywhere-electron\"))] | sort'\ngh release view v<version> --repo RunanywhereAI/runanywhere-sdks --json assets \\\n    --jq '[.assets[].name | select(test(\"qhexrt\";\"i\"))]'          # MUST be []\n```\n\n### Prove the artifacts are real — don't trust green checkmarks\n\nA green `publish` only proves the assertions ran. To prove the bytes are right, run the\nrepo's own fail-closed gate against the **published** tarballs. This is the single highest\n-value verification in the whole runbook and it takes about a minute:\n\n```bash\ngh release download v<version> --repo RunanywhereAI/runanywhere-sdks \\\n    -p 'runanywhere-electron*.tgz' -D /tmp/verify\npython3 scripts/validation/gates/check_plugin_natives.py --expect-version <version> \\\n    $(for t in /tmp/verify/*.tgz; do printf -- '--tarball %s ' \"$t\"; done)\n```\n\nExpect `All N check(s) passed: plugin natives are routable and no staging-URL placeholder\nwas found.` On v0.20.25 that was **20 checks** across 5 tarballs, asserting three separate\nthings per binary: the real `STAGING_BASE_URL` is baked in (no `YOUR_STAGING_BASE_URL`\nplaceholder), `rac_commons` embeds the right version string, and each backend is actually\n**routable** (real op tables — `g_llamacpp_ops`, `g_sherpa_stt_ops`, `g_neurt_llm_ops`,\netc.), which is what distinguishes a real plugin from a same-sized shell.\n\nSanity-check the shipped layout too — a tarball missing a platform is a silent regression:\n\n| Package | Expected `prebuilds/` |\n|---|---|\n| `@runanywhere/electron` | `darwin-arm64` + `win32-x64` + `win32-arm64` |\n| `-llamacpp`, `-onnx`, `-sherpa` | `darwin-arm64` + `win32-x64` |\n| `-neurt` | `darwin-arm64` only (Apple Neural Engine) |\n| `-qhexrt` (private artifact) | `win32-arm64` only (Hexagon NPU) |\n\n---\n\n## 9. Windows handling — build/preserve only, never auto-publish\n\n`native_windows` and `native_rcli_windows` run and their outputs are **preserved** as\nrelease assets (proving the binaries build), but nothing Windows-specific is published to\nany package registry as part of the standard flow unless a human explicitly asks for it —\nthat's a deliberate, separate decision, not an oversight.\n\nFor anything that needs actual execution on Windows (smoke-testing the native Windows\nbuild, or Electron's Windows packaging), do not assume a macOS cross-build proves\nWindows correctness — SSH to the real Windows box. The team keeps a **Windows ARM64\nmachine reachable over SSH**; its hostname, user, key, and mesh-VPN node name are\noperator-specific and deliberately not recorded in this repo — read the Windows/ARM64\nhost entry out of the operator's `~/.ssh/config` and use that alias. If there is no such\nentry, the box is not configured for you: say so and ask, rather than guessing.\n\n```bash\nWIN_BOX=<the ssh alias from ~/.ssh/config>\n# Ask the mesh-VPN client whether the node is up FIRST (e.g. `tailscale status`,\n# matching on that host entry's HostName) — never just try SSH and retry on timeout.\nssh -o ConnectTimeout=8 \"$WIN_BOX\" \"whoami\"\n```\n\nIf the VPN reports the node offline/asleep, **that's not a config problem** — ask the\nuser to wake the box; don't debug SSH config against a machine that's simply off.\n\n---\n\n## 10. Publish the draft, then hand off\n\nOnce the draft's asset set is verified complete and correct (step 8):\n\n```bash\ngh release edit v<version> --repo RunanywhereAI/runanywhere-sdks --draft=false --latest\n```\n\nThis is the point where SwiftPM's `from: \"<version>\"` resolution and the download URLs\nbaked into `Package.swift` start actually working for real external consumers — draft\nrelease assets are not publicly downloadable, so nothing before this line is\nexternally live.\n\n**Hand off to the `sdk-publish` skill** for the actual npm / Maven Central / pub.dev /\nSwiftPM-distribution-repo publishing steps. This skill's job ends here.\n\n---\n\n## 10b. MANDATORY, and it turns `main` RED until you do it — cut runanywhere-swift\n\nThe moment `v<version>` exists in this repo, `check_swift_dist_repo_sync.sh` (run by\n`pr-build.yml`'s `centralization` job) **fails on every PR and every push to `main`**:\n\n```\n[FAIL] runanywhere-swift has no 0.20.25 tag (latest: 0.20.24)\n       v0.20.25 is published here, so 'from: \"0.20.25\"' is broken for every\n       SwiftPM consumer of https://github.com/RunanywhereAI/runanywhere-swift.git\n```\n\nThis is **by design**, not a bug, and it is the #1 reason a release looks \"not green\"\nafter everything else succeeded. Do not go hunting for a code defect — cut the repo:\n\n```bash\ngit clone https://github.com/RunanywhereAI/runanywhere-swift.git /tmp/ra-swift\nbindings/swift/scripts/sync-dist-repo.sh --zips <zip_dir> --tag /tmp/ra-swift\n# verify BEFORE pushing (content check passes locally; the tag check needs the push)\nRUNANYWHERE_SWIFT_DIST_REPO=/tmp/ra-swift \\\n    bash scripts/validation/gates/check_swift_dist_repo_sync.sh\ngit -C /tmp/ra-swift push origin main --follow-tags\nbash scripts/validation/gates/check_swift_dist_repo_sync.sh    # now [OK]\n```\n\nGotchas found doing this on 0.20.25:\n\n- `--check` **cannot be combined** with `--zips`/`--commit`/`--tag` (hard error). Run a\n  bare `--check <repo>` first to preview, then the real invocation separately.\n- `--check` legitimately reports `ERROR: distribution repo is out of sync` before you\n  sync — that's the preview telling you what it will change (expect the `sdkVersion` bump,\n  ~3 changed files, `0 added / 0 removed`). Non-zero exit there is not a failure.\n- The script **never pushes**; `--tag` only creates the tag locally. The dist repo tag has\n  **no `v` prefix** (`0.20.25`, not `v0.20.25`) while the monorepo tag does.\n- Running the gate locally right after `--tag` still shows `[FAIL] ... has no <version>\n  tag` while simultaneously showing `[OK] ... checkout matches: all 6 checksums`. That is\n  expected: the content check reads your local checkout, the tag check queries the real\n  remote. It clears the moment you push.\n- It chains into `sync-checksums.sh`, so run it **after** step 3's checksums are committed,\n  or the dist manifest gets stale hashes.\n\n---\n\n## 10c. Windows + self-hosted-runner traps (read before debugging a red Windows job)\n\nThese all cost a full CI cycle. The unifying fact:\n\n> The self-hosted runner service executes as `NT AUTHORITY\\NETWORK SERVICE`, which has its\n> own PATH **and** its own NTFS permissions. Anything you verify over SSH on that box tells\n> you nothing about what the job can do. `native_win_arm64_qhexrt` already carries a long\n> comment about this — read it before inventing a fix, and keep it in sync with neurun's\n> equivalent lane, which runs on the *same machine*.\n\n**Environment**\n\n- **`bash: command not found`** — Git for Windows puts `Git\\cmd` (git.exe) on PATH but not\n  `Git\\bin` (bash.exe). `actions/checkout` still works because it calls git.exe directly,\n  which makes this look unrelated to runner setup.\n- **`pwsh` does not exist on that box** — Windows PowerShell 5.1 only. Use\n  `shell: powershell`.\n- **`gh` is not installed on that box, and must not be assumed anywhere.** A QHexRT\n  download died on `gh: command not found`. The fix is deliberately *not* to install it:\n  every fetch of a private release asset now goes through\n  `fetch_release_asset()` in `scripts/build/_release_asset.sh`, which uses `curl` + the\n  REST API. `curl` ships with Windows, macOS and every Linux image we use, so this is one\n  code path everywhere instead of a per-runner provisioning step. If you add a new\n  consumer of a private asset, source that helper — do not reach for `gh`.\n  Note the helper matches the asset name **exactly**: a prefix match picks the `.sha256`\n  sidecar (its name starts with the tarball's), and you get a 65-byte \"archive\" that fails\n  much later with a confusing error.\n- **Tool availability is the single most common failure class on this runner** — `bash`,\n  `python3`, `ninja`, `gh`, GNU `tar` and NDK `.exe` suffixes have each broken a lane\n  once. When a self-hosted job fails, check \"does this tool exist as spelled\" before\n  reading any logic.\n- **`ln -s` under Git-Bash COPIES the directory** unless `MSYS=winsymlinks:nativestrict`.\n  It does not fail, so the result looks like real content and whatever validates a\n  \"must be a symlink\" invariant rejects a payload that was actually fine. Never use shell\n  `ln -s` on this box — go through `scripts/build/_selection.py`.\n- **Windows selects with a JUNCTION, not a symlink, and that is deliberate.** The runner\n  service account cannot create symlinks (below), but a junction is the same kind of\n  reparse point to a directory and needs no privilege. Two traps when reading that code:\n  `Path.is_symlink()` returns **False** for a junction (`stat.S_ISLNK` is true only for\n  `IO_REPARSE_TAG_SYMLINK`) — use `_selection.is_selection()`; and a junction stores an\n  **absolute** t",".claude/skills/engine-plugin-authoring/SKILL.md":"---\nname: engine-plugin-authoring\ndescription: Step-by-step recipe for adding a new engine plugin under runanywhere-sdks/engines/, or adding a modality (e.g. TTS) to an existing engine (llamacpp, sherpa, onnx, cloud, mlx, neurt, qhexrt). Use when the task is \"add engine X\", \"wire up backend Y\", or \"make engine Z also serve <modality>\".\n---\n\n# Engine plugin authoring (`engines/`)\n\nTwo related runbooks: adding a brand-new engine plugin, and adding a modality to one\nthat already exists. Read `engines/AGENTS.md` first for the concepts these steps\nassume (op-table vtable, manifest, the valid-engine checklist, the 4-file skeleton,\nthe engine↔runtime rule). This skill is the \"how\", that file is the \"what/why\".\n\n**Invariant that applies to both procedures:** never invent a new modality just to\nship an engine — fill an existing `rac_engine_vtable_t` slot. Adding a brand-new\n*primitive* (a new `RAC_PRIMITIVE_*` + vtable slot) is a commons ABI change, covered\nby `core/AGENTS.md` → \"Adding a new capability interface\", not an engine change.\n\n---\n\n## Procedure A — add a new engine\n\nMirror **sherpa** (multi-modality, standalone `.so`) or **cloud** (single modality,\nno runtime) as your template; both are the cleanest references for the 4-file\nskeleton described in `engines/AGENTS.md`.\n\n1. **Create `engines/<name>/`** with the 4-file skeleton: `rac_plugin_entry_<name>.cpp`,\n   `rac_backend_<name>_register.cpp` (skip it if there's no extra bring-up, like\n   `neurt`), `rac_static_register_<name>.cpp`, plus your impl files.\n2. **Write the manifest** in `rac_plugin_entry_<name>.cpp`:\n   - `name` = snake_case identity (the library/framework you wrap, or your own\n     codebase unit — NEVER a modality name; see the naming rule in\n     `engines/AGENTS.md`).\n   - `primitives[]` = what you serve; fill the matching vtable slots; leave every\n     other slot explicit NULL (the all-NULL tripwire depends on this).\n   - `runtimes[]` — only devices your compute actually depends on (NULL for\n     cloud/HTTP engines) — see THE RULE in `engines/AGENTS.md`.\n   - `formats[]` = the `RAC_MODEL_FORMAT_ID_*` values you accept (NULL if there's\n     no local model file).\n   - `priority`, `availability` (PUBLIC/PRIVATE), `package_owner`/`package_name`.\n3. **Implement the op-table(s)** honoring the MODEL LIFECYCLE:\n   `create → initialize → use → cleanup → destroy` (see `engines/AGENTS.md` for the\n   exact signatures). Use `RAC_DEFINE_CREATE_ADAPTER(primitive, name)`\n   (`core/include/rac/plugin/rac_plugin_entry.h`) when `create` is a plain forward\n   onto `rac_<primitive>_<name>_create` — sherpa uses it for STT/TTS/VAD. Engines\n   with richer create flows (llamacpp, onnx, neurt, qhexrt) hand-write it.\n4. **`capability_check`** when the engine is platform- or binary-gated: use the\n   shared 3-way helper `rac_engine_unavailable_capability(platform_supported,\n   backend_present)` from `engines/common/rac_engine_unavailable.h`. When the\n   backend can be entirely absent at build time (a private prebuilt archive, a\n   sibling private repo), emit the stub with `RAC_ENGINE_UNAVAILABLE_PLUGIN(name,\n   display, cap_fn)` from the same header — see qhexrt and neurt for the two-mode\n   (ROUTABLE vs STUB) pattern this produces.\n5. **Registration carriers:** `RAC_PLUGIN_ENTRY_DEF(<name>)` returning the vtable; a\n   `rac_backend_<name>_register()`/`_unregister()` pair if you have extra bring-up\n   (called directly by SDK bridges on dynamic-link hosts); a\n   `rac_static_register_<name>.cpp` shim using `RAC_STATIC_REGISTER_BACKEND(<name>)`\n   (routes through the register fn) or `RAC_STATIC_PLUGIN_REGISTER(<name>)` (calls\n   the entry directly) for iOS/WASM static hosts.\n6. **CMake:** call `rac_add_engine_plugin(<name> SOURCES … LINK_LIBRARIES …\n   AVAILABILITY … PACKAGE_OWNER … PACKAGE_NAME …)` (`cmake/plugins.cmake`) from\n   `engines/<name>/CMakeLists.txt`, fronted by your own\n   `option(RAC_BACKEND_<NAME> \"…\" <default>)` + `if(NOT RAC_BACKEND_<NAME>)\n   return() endif()` self-gate (each engine owns its own option — `engines/CMakeLists.txt`\n   just unconditionally `add_subdirectory()`s and lets the child decide). Add\n   `add_subdirectory(<name>)` there. Declare `primitives`/`runtimes`/`formats` ONLY\n   in the C manifest, never in CMake, so the build graph can't drift from what the\n   router routes.\n7. **Android:** if the SDK loads the engine via `System.loadLibrary` +\n   `nativeRegister`, add a JNI bridge with `RAC_DEFINE_ENGINE_JNI_BRIDGE(...)`\n   (standalone per-engine `.so` — onnx, llamacpp) or\n   `RAC_DEFINE_ENGINE_JNI_BRIDGE_NO_ONLOAD(...)` (folded into a host lib that\n   already owns `JNI_OnLoad`, e.g. cloud → `librunanywhere_jni.so`) from\n   `engines/common/rac_engine_jni_bridge.h`. The JVM class-path token in the macro\n   call must match the Kotlin `*Bridge` class byte-for-byte.\n8. **iOS/WASM static hosts:** make sure the host force-loads the plugin —\n   `rac_force_load(<app> PLUGINS <name>)` in the host's CMakeLists — so the\n   static-init Registrar TU survives linker stripping.\n9. **If this engine ships an Electron/npm carrier package**, register it in\n   `scripts/validation/gates/check_plugin_natives.py`'s `REQUIRED_OPS` dict (the\n   ops-table symbol name(s) your entry TU references only on the routable\n   branch) — `package-sdk.sh` calls this automatically, so an unregistered\n   engine's stub/shell builds ship undetected. **Check which FILE actually\n   compiles `rac_plugin_entry_<name>.cpp`** before assuming the carrier\n   (`runanywhere_<name>`) is what to gate: sherpa/onnx/llamacpp compile it into\n   the carrier itself (an undefined symbol reference there is the evidence),\n   but neurt's `rac_add_engine_plugin()` call puts it on the ENGINE target\n   (`rac_backend_neurt`) instead — the carrier there is empty boilerplate with\n   zero symbols. Add the engine id to `EVIDENCE_IN_BACKEND_FILE` if it follows\n   neurt's shape; discovered by actually building neurt for real and running\n   `nm -a` on both files, not by reading the CMakeLists and assuming.\n\n## Procedure B — add a modality to an existing engine\n\nMulti-modality is the whole point of an identity-named engine. `cloud` and `neurt`\nare explicitly built for this and carry the recipe inline as a comment next to\ntheir vtable.\n\n1. Implement the new modality's op-table (e.g. a cloud TTS `g_cloud_tts_ops`,\n   typically backed by a per-modality provider adapter under `providers/`).\n2. **Fill the corresponding slot** in the engine's `rac_engine_vtable_t`\n   (`tts_ops` / `llm_ops` / `embedding_ops` / …) — every other slot must stay\n   explicit NULL.\n3. **Add the primitive** to the manifest's `primitives[]`.\n4. Update `formats[]`/`runtimes[]` only if the new modality actually needs them\n   (per THE RULE — don't declare a runtime your new op-table doesn't depend on).\n\nNo new plugin, no rename, no ABI bump — the engine already owns one\n`rac_engine_vtable_t` with all ten primitive slots; you're just filling another one.\nGrep the target engine's `rac_plugin_entry_<name>.cpp` for the phrase \"To add a\"\n— cloud and neurt both leave a comment naming exactly which slots are still free\nand what to add to `primitives[]`.\n",".agents/skills/sdk-publish/SKILL.md":"---\nname: sdk-publish\ndescription: Manually publish an already-tagged, already-released (non-draft GitHub Release) RunAnywhere SDK version to every package registry (npm, Maven Central, pub.dev, Swift SPM dist repo) plus macOS rcli notarization, with heavy verification that nothing fake/stubbed/broken ships.\n---\n\n# SDK Publish\n\nRepo: `~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks` (verify with `pwd`/`git remote get-url origin` — do not assume this path is still correct, checkouts move). Version source of truth: `core/VERSION`. Throughout, `$VERSION` means the bare semver being published (e.g. `0.20.24`), never prefixed with `v` except in git tags / GitHub Release names.\n\n## THIS PROCESS IS INTENTIONALLY MANUAL\n\n**`release.yml` never publishes anywhere public.** It only builds artifacts, packages them, and creates a draft/published **GitHub Release**. It does not call `npm publish`, does not upload to Maven Central, does not call `flutter pub publish`, and does not push the Swift dist repo. Verify this is still true before trusting it — greps below should all return **zero** matches (grep the whole file, not a snippet):\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks\ngrep -inE \"npm publish|publishReleasePublication|publishAllPublicationsToMavenCentral|flutter pub publish\" .github/workflows/release.yml\n```\n\nThis is a **deliberate safety gate**, not a gap. Every step below is a human (or an agent acting on a human's explicit go-ahead) pushing a button that CI intentionally never pushes. Do not propose wiring any of this into CI as a \"fix\" — that would remove the gate.\n\n## Precondition check (always run first)\n\nRefuse to proceed if the release is a draft or the asset set looks incomplete.\n\n```bash\ngh release view v$VERSION --json isDraft,tagName --jq '{isDraft, tagName}'\n# isDraft MUST be false.\n\ngh release view v$VERSION --json assets --jq '.assets[].name' | sort\n```\n\nCross-reference the listing against the `sdk-release` skill's step 8 asset-count expectations (`~/.claude/skills/sdk-release/SKILL.md`). As a second, independent check, also diff against the **previous** tagged release's asset list — same count, same name patterns, only the version token differs:\n\n```bash\ngh release view v<PREVIOUS_VERSION> --json assets --jq '.assets[].name' | sed \"s/<PREVIOUS_VERSION>/$VERSION/\" | sort >/tmp/expected.txt\ngh release view v$VERSION --json assets --jq '.assets[].name' | sort >/tmp/actual.txt\ndiff /tmp/expected.txt /tmp/actual.txt\n```\n\nAs of the last verified run (**v0.20.25, 65 assets** — up from 55 at v0.20.24 because the 5 public Electron tarballs and their sidecars joined the release; see §6), the full set is: `MANIFEST.txt`; per-platform `RACommons-{android-arm64-v8a,android-armeabi-v7a,android-x86_64,ios,linux-x86_64,web,windows-x64}-v$VERSION.{zip,tar.gz}` + `.sha256`; iOS backend zips `RABackend{LLAMACPP,MLX,NeuRT,ONNX,Sherpa}-ios-v$VERSION.zip` + `.sha256`; `RunAnywhereMLX{Metal,Resources,Runtime}-ios-v$VERSION.zip` + `.sha256`; `rcli-{linux-x86_64,macos-arm64,windows-x86_64}-v$VERSION.tar.gz`/`.zip` + `.sha256`; the 8 npm tarballs `runanywhere-{core,llamacpp,mlx,onnx,proto-ts,web,web-llamacpp,web-onnx}-$VERSION.tgz` + `.sha256`; the **5 Electron npm tarballs** `runanywhere-electron{,-llamacpp,-onnx,-sherpa,-neurt}-$VERSION.tgz` + `.sha256` (new as of v0.20.25); and `runanywhere-kotlin-maven-v$VERSION.zip` + `.sha256`. There is **no** Flutter tarball in the release (Flutter packages are staged from the native archives above, not from a dedicated release asset) and **no** `.dmg` asset in the baseline set (the macOS rcli tarball is what ships; see the notarization section for when a `.dmg` does get produced). If a `qhexrt`-named asset appears anywhere in this list, stop — that would mean `release.yml`'s own manifest assertion should have hard-failed and something is very wrong; do not publish anything from that release.\n\n## 0. MANDATORY GATE — verify the ENTIRE release before pushing anything to any registry\n\n**Run this once, in full, before the first registry push. Do not publish a single package until it passes.** Per-package spot-checks are not a substitute: they are how a bad artifact reaches a registry you cannot un-publish from. Proven end to end on v0.20.25 (2026-08-22), then independently audited and corrected: **34 payloads, 6619 files across 3 nesting levels, 282 binaries, 0 placeholder hits, 38/38 commons binaries carrying the real staging host**. The audit found 4 methodology gaps in the first pass (shallow nesting, missing `MTLB` magic, `.lib` excluded from the positive check, and a set-intersection that could pass vacuously) — every one is called out inline below, because each produced a *confident but under-covered* result.\n\n```bash\nVERSION=0.20.25                      # no leading v\nREPO=RunanywhereAI/runanywhere-sdks\nW=/tmp/relverify-$VERSION\nrm -rf \"$W\" && mkdir -p \"$W/assets\" \"$W/qhexrt\" \"$W/x\" \"$W/nested\"\n\n# (1) EVERY release asset — no -p filter, get the whole set.\ngh release download \"v$VERSION\" --repo \"$REPO\" -D \"$W/assets\"\n\n# (2) The PRIVATE QHexRT artifact is NOT a release asset — pull it from the run.\n#     Find the run that produced the release, then:\ngh run download <release-run-id> --repo \"$REPO\" --name sdk-electron-qhexrt-private --dir \"$W/qhexrt\"\n```\n\n### (3) Checksums — `cd` into the directory first\n\nSidecars contain a **bare basename**, so `shasum -c` fails with a misleading \"FAILED open or read\" if run from anywhere else. This will make every artifact look broken when nothing is wrong:\n\n```bash\ncd \"$W/assets\"\nok=0; bad=0\nfor s in *.sha256; do\n  if shasum -a 256 -c \"$s\" >/dev/null 2>&1; then ok=$((ok+1)); else echo \"MISMATCH: $s\"; bad=$((bad+1)); fi\ndone\necho \"verified=$ok mismatched=$bad\"      # bad MUST be 0\n(cd \"$W/qhexrt\" && shasum -a 256 -c ./*.sha256)\n```\n\n### (4) Extract everything — INCLUDING nested archives\n\n**The gotcha that matters:** `runanywhere-kotlin-maven-v*.zip` contains `.aar` files, and the `.aar`s are what hold the Android `.so` binaries. A one-level extraction reports \"0 binaries\" for the Maven bundle and every subsequent scan passes **vacuously**.\n\n**Recurse until the tree is dry — TWO levels are required here, not one.** Each `.aar` also contains a `classes.jar`, holding ~2027 `.class` files that are doubly unreachable: inside an unopened jar *and* excluded by the `.class` suffix rule most magic-byte scanners use. Level 2 adds **2033 files** (0 placeholder hits on v0.20.25); level 3 is empty. Loop until a pass finds no new archives rather than hardcoding a depth.\n\n```bash\ncd \"$W/assets\"\nfor f in *; do\n  case \"$f\" in *.sha256|MANIFEST.txt) continue;; esac\n  d=\"$W/x/$f\"; mkdir -p \"$d\"\n  case \"$f\" in\n    *.zip)              unzip -qq -o \"$f\" -d \"$d\" || echo \"EXTRACT FAILED: $f\" ;;\n    *.tar.gz|*.tgz)     tar -xzf \"$f\" -C \"$d\"     || echo \"EXTRACT FAILED: $f\" ;;\n    *) echo \"UNHANDLED TYPE: $f\" ;;\n  esac\ndone\nd=\"$W/x/$(basename \"$W\"/qhexrt/*.tgz)\"; mkdir -p \"$d\"; tar -xzf \"$W\"/qhexrt/*.tgz -C \"$d\"\n\n# nested pass\ncd \"$W/x\"\nfind . -type f \\( -name '*.aar' -o -name '*.jar' -o -name '*.zip' -o -name '*.tgz' -o -name '*.tar.gz' \\) \\\n  | sed 's|^\\./||' | while IFS= read -r a; do\n    dd=\"$W/nested/$(printf '%s' \"$a\" | tr '/' '_')\"; mkdir -p \"$dd\"\n    case \"$a\" in *.aar|*.jar|*.zip) unzip -qq -o \"$a\" -d \"$dd\";; *) tar -xzf \"$a\" -C \"$dd\";; esac\n  done\n```\n\n**Prove extraction actually happened** — an empty dir makes \"0 hits\" meaningless:\n\n```bash\nfor d in \"$W\"/x/*/; do\n  n=$(find \"$d\" -type f | wc -l)\n  [ \"$n\" -lt 2 ] && echo \"SUSPICIOUSLY EMPTY: $d ($n files)\"\ndone\n```\n\n### (5) Placeholder scan — EVERY file, not just binaries\n\nThe staging URL can also live in JS/JSON/TS config, so scan by content across all file types, and additionally scan the raw compressed streams in case a member never got extracted:\n\n```bash\nfind \"$W/x\" \"$W/nested\" -type f | wc -l                                     # total files covered\ngrep -rla \"YOUR_STAGING_BASE_URL\" \"$W/x\" \"$W/nested\" | tee /tmp/ph.txt | wc -l   # MUST be 0\nfor f in \"$W\"/assets/*.tgz \"$W\"/assets/*.tar.gz \"$W\"/qhexrt/*.tgz; do\n  gzip -dc \"$f\" 2>/dev/null | grep -qa \"YOUR_STAGING_BASE_URL\" && echo \"RAW HIT: $f\"\ndone\n```\n\nFind binaries by **magic bytes, not extension** (an unsuffixed framework binary like `RACommons.framework/RACommons` has no extension and a name-based `find` misses it). Signatures to cover:\n\n| Magic | Kind | v0.20.25 count |\n|---|---|---|\n| `7f454c46` | ELF | 177 |\n| `!<arch>` (`213c617263683e0a`) | static `.a` **and `.lib`** | 41 |\n| `MZ` | PE | 29 |\n| `cffaedfe` / `cefaedfe` | Mach-O | 19 |\n| `00 61 73 6d` | wasm | 10 |\n| **`4d544c42` (`MTLB`)** | **Metal library** — easy to miss | 5 |\n| `cafebabe` / `bebafeca` | fat Mach-O (exclude `.jar`/`.class`) | 1 |\n\n**Total: 282 binaries.** An earlier pass reported 277 because `MTLB` was absent from the signature list — `.metallib` files were invisible to the binary scan (they were still covered by the all-file `grep` in step 5, so the conclusion held, but the count was wrong). If your total comes in materially under ~282 for a comparable release, your detection is leaking.\n\n### (6) POSITIVE staging-URL check — absence of the placeholder is NOT sufficient\n\nA build with an **empty** URL baked in passes a placeholder-only check. `check_plugin_natives.py` only tests absence, so add the positive assertion yourself.\n\nTwo traps that produced false confidence on the v0.20.25 pass, both worth avoiding:\n\n- **Include `.lib`.** A `MUST_HAVE` regex of `rac_commons\\.(dylib|so|dll|a)$` silently skips\n  `RACommons-windows-x64/x64/rac_commons.lib` — an **89 MB** binary that then gets no positive\n  check at all. Match `(dylib|so|dll|a|lib)` plus `\\.wasm$`. Correct coverage is **38** binaries,\n  not 37.\n- **Assert per binary, not by set-intersection.** Intersecting \"non-public origins across all\n  files\" yields more than one origin here (`hf.co` *and* the staging host), so a binary carrying\n  only `hf.co` would pass **vacuously**. Check each binary individually for the staging host.\n\n```bash\n# v0.20.25 baseline: 38 commons/wasm/lib binaries; 0 missing the staging host.\n```\n\nNever print the URL — it is a repo secret. Report a truncated SHA-256 fingerprint, or just assert presence/absence counts.\n\n### (7) Routability gate over the whole tree\n\n```bash\npython3 scripts/validation/gates/check_plugin_natives.py \"$W/x\" \"$W/nested\" > /tmp/gate.txt 2>&1\ngrep -cE '^  ok ' /tmp/gate.txt; grep -E '^  FAIL' /tmp/gate.txt\n```\n\nv0.20.25 baseline: **77 ok, 1 FAIL**. The single expected FAIL is\n`RACommons-linux-x86_64/lib/librunanywhere_sherpa.so` — see the known-exception note below. **Any other FAIL blocks the publish.**\n\n### (8) Known exception — Linux sherpa is a non-routable shell (PRE-EXISTING, not a regression)\n\n`RACommons-linux-x86_64-v*.tar.gz` ships **no** `libsherpa-onnx-c-api.so` at all and its `librac_backend_sherpa.so` is a ~156 KB shell (~3 sherpa strings, vs ~31 MB / ~192 for a healthy one on Electron macOS) — the engine is not linked in, so the carrier cannot reference `g_sherpa_{stt,tts,vad}_ops` and **STT/TTS/VAD cannot work from that bundle**. Android's carriers pass on all 3 ABIs, so this is Linux-only.\n\n**Compare the CARRIER, not the backend.** The gate FAILs on `librunanywhere_sherpa.so` (the carrier); citing `librac_backend_sherpa.so` as your evidence is comparing a *different file* and is not proof. The carrier is byte-identical across releases, which is the real proof:\n\n```bash\ngh release download \"v<PREV>\" --repo \"$REPO\" -p 'RACommons-linux-x86_64-v<PREV>.tar.gz' -D /tmp/prev\ntar -xzf /tmp/prev/*.tar.gz -C /tmp/prev\nshasum -a 256 /tmp/prev/lib/librunanywhere_sherpa.so \"$W\"/x/RACommons-linux-*/lib/librunanywhere_sherpa.so\n# v0.20.24 and v0.20.25 both: size=15712  sha256=28ef16015b61da86...\n\n# strongest check — run the gate on the PREVIOUS bundle and confirm the identical FAIL:\npython3 scripts/validation/gates/check_plugin_natives.py /tmp/prev\n```\n\nIf it ever regresses *further* (e.g. Android or macOS sherpa also goes hollow), stop and treat it as a blocking defect.\n\n### (9) Account for every payload explicitly\n\nPrint a per-artifact table (checksum verdict, binary count, placeholder hits) and confirm the row count equals the payload count. Artifacts that legitimately contain **no** native binaries, and why — do not let these pass silently unexamined:\n\n| Artifact | Why it has no binaries |\n|---|---|\n| `RunAnywhereMLXResources-ios-*.zip` | 25 third-party license/notice text files |\n| `runanywhere-kotlin-maven-v*.zip` | binaries live in the **nested** `.aar`s (step 4) |\n| `runanywhere-proto-ts-*.tgz` | pure TS/JS (69 `.ts` + 69 `.js`) |\n\nEverything else must have a non-zero, plausible binary count. A platform suddenly shipping fewer binaries than the previous release is a regression signal worth chasing before publishing.\n\n### (10) Blind spot to be aware of — Windows x64 has NO routability coverage\n\n`RACommons-windows-x64-v*.zip` ships **only** a static `rac_commons.lib` (~89 MB) plus 152 `.h` / 11 `.hpp` headers — **zero backend DLLs**, where Linux/Android/Web each ship 4–5 backend carriers. That is consistent with a static-embedding bundle (consumers link the engines in themselves), but the consequence matters: **no gate can detect a missing or hollow Windows backend**, because there is no carrier to inspect. `check_plugin_natives.py` therefore reports nothing at all for this artifact rather than reporting health.\n\nWindows x64 is also *advisory* in `release.yml` (its absence does not fail the release). So a Windows regression can pass every automated check. If Windows correctness matters for a given release, verify it by actually running something on the real Windows box (see §Electron for how to reach it) — do not infer it from a green pipeline. Worth a deliberate confirmation with the repo owner that the DLL-free shape is intentional.\n\n---\n\n## General verification discipline (every artifact, every registry)\n\nApply all three checks below to **every** file you're about to publish, before AND after the publish call. Never trust a copy cached locally earlier in the session — re-download.\n\n**(a) Download + verify the sha256 sidecar yourself:**\n\n```bash\ngh release download v$VERSION -p '<file>' -p '<file>.sha256' -D /tmp/sdk-publish-$VERSION\ncd /tmp/sdk-publish-$VERSION && shasum -a 256 -c '<file>.sha256'\n```\n\n**(b) Inspect the actual file listing inside every tarball/AAR/zip** — never assume from the filename alone:\n\n```bash\nnpm pack <tgz-or-package-dir> --dry-run     # npm packages: prints the exact file list it would publish\nunzip -l  <file.zip>                        # AARs, Maven repo zip, native archives\ntar -tzf  <file.tar.gz>                     # rcli, RACommons tarballs\n```\n\nSanity-check what comes back:\n- No suspiciously-small or empty binaries where a real native library (`.so`, `.dylib`, `.a`, `.dll`) should be — a stub or a zero-byte placeholder means the build silently failed upstream.\n- No leftover `workspace:*` or `file:` dependency specs in any `package.json` inside the tarball — those only resolve inside this monorepo and break for a real external installer.\n- Version strings inside every manifest (`package.json` `version`, AAR `pom.xml` `<version>`, `pubspec.yaml` `version:`) **exactly** match `$VERSION`.\n- **Hard rule, no exceptions:** grep the full file listing for `qhexrt` (any case) and for QNN binary names (`libQnnHtp`, `QnnHtp*.dll`, `rac_commons.dll` bundled with QHexRT, `runanywhere_qhexrt`). If any hit appears in a package meant to be public, stop and do not publish that artifact.\n\n**(c) Confirm baked-in runtime config** (e.g. a staging-vs-prod backend base URL compiled into `rac_commons` at build time) points at the real intended endpoint. This is designed to fail closed if misconfigured — a build that completed and produces working requests against prod is itself decent evidence — but if you have any doubt, `strings <binary> | grep -i \"api\\.\\|staging\\.\"` and eyeball it.\n\n## 1. npm — 8 packages, order matters\n\nPackages: 4 Web (`@runanywhere/proto-ts`, `@runanywhere/web`, `@runanywhere/web-llamacpp`, `@runanywhere/web-onnx`) + 4 React Native (`@runanywhere/core`, `@runanywhere/llamacpp`, `@runanywhere/mlx`, `@runanywhere/onnx`). Never `@runanywhere/qhexrt` (RN) — it exists in `bindings/react-native/packages/qhexrt/` but is excluded from the public npm train by the same policy as Maven/pub.dev QHexRT.\n\n**Publish `@runanywhere/proto-ts` FIRST.** `@runanywhere/web-onnx` deliberately does not bundle its own `proto-ts` copy and needs it resolvable from the live registry for real consumers; publish `proto-ts` before it or a fresh `npm install @runanywhere/web-onnx` fails to resolve.\n\n```bash\ngh release download v$VERSION -p 'runanywhere-proto-ts-*.tgz*' -p 'runanywhere-web-*.tgz*' \\\n  -p 'runanywhere-core-*.tgz*' -p 'runanywhere-llamacpp-*.tgz*' -p 'runanywhere-mlx-*.tgz*' -p 'runanywhere-onnx-*.tgz*' \\\n  -D /tmp/sdk-publish-$VERSION\ncd /tmp/sdk-publish-$VERSION\nfor f in *.sha256; do shasum -a 256 -c \"$f\"; done\n\nnpm publish runanywhere-proto-ts-$VERSION.tgz --access public   # FIRST, always\nnpm publish runanywhere-web-$VERSION.tgz --access public\nnpm publish runanywhere-web-llamacpp-$VERSION.tgz --access public\nnpm publish runanywhere-web-onnx-$VERSION.tgz --access public\nnpm publish runanywhere-core-$VERSION.tgz --access public\nnpm publish runanywhere-llamacpp-$VERSION.tgz --access public\nnpm publish runanywhere-mlx-$VERSION.tgz --access public\nnpm publish runanywhere-onnx-$VERSION.tgz --access public\n```\n\n`npm whoami` should already show a logged-in account; if it errors, stop and tell the human rather than trying to log in non-interactively.\n\n**GOTCHA — large tarballs can silently die mid-upload.** `@runanywhere/mlx` (~50MB) and `@runanywhere/web-onnx` (unpacks to 300+MB) can exceed a short default shell/tool timeout mid-upload and get killed with no clean error. Use an explicit long timeout (5 minutes / 300000ms) on each publish call.\n\n**GOTCHA — verify via the registry API, not the npm CLI.** `npm view` can lag the CLI's own cache by several seconds right after a fresh publish and falsely report \"not found\". After **every** publish:\n\n```bash\ncurl -s \"https://registry.npmjs.org/@runanywhere/<pkg>\" | python3 -c \"import json,sys;print(json.load(sys.stdin)['dist-tags'])\"\n```\nConfirm `latest` equals `$VERSION`.\n\n## 2. Maven Central — 3 AARs only\n\n`io.github.sanchitmonga22:{runanywhere-sdk,runanywhere-llamacpp,runanywhere-onnx}`. **Never `runanywhere-qhexrt-android`** — it was published through 0.20.19 under an earlier, since-reversed policy and is now permanently excluded; `release.yml`'s manifest assertion hard-fails if it reappears in the public asset set.\n\nFull step-by-step lives in `bindings/kotlin/docs/KOTLIN_MAVEN_CENTRAL_PUBLISHING.md`, section **\"Option A: Publish with released native archives\"** (line ~108 as of this writing — the doc has been corrected mid-release before, so re-read it fresh rather than trusting memory of its contents). Key points, condensed:\n\n1. Download + verify the 3 `RACommons-android-{arm64-v8a,armeabi-v7a,x86_64}-v$VERSION.zip` from the release (sha256 sidecars, per the general discipline above).\n2. Stage: `bash bindings/kotlin/scripts/package-sdk.sh --mode local --natives-from <dir-with-the-3-zips>`. This also produces a local Maven-repo zip — diff its size against `runanywhere-kotlin-maven-v$VERSION.zip` already attached to the GitHub Release as a cheap reproducibility check.\n3. Before uploading, confirm the OSSRH compatibility service has no dangling open repository for this namespace:\n   ```bash\n   CENTRAL_BEARER=\"$(printf '%s:%s' \"$MAVEN_CENTRAL_USERNAME\" \"$MAVEN_CENTRAL_PASSWORD\" | base64 | tr -d '\\r\\n')\"\n   curl -s -H \"Authorization: Bearer $CENTRAL_BEARER\" \\\n     'https://ossrh-staging-api.central.sonatype.com/manual/search/repositories?ip=client&profile_id=io.github.sanchitmonga22'\n   ```\n   Resolve any stale repository per the doc's troubleshooting table before continuing.\n4. Source `MAVEN_CENTRAL_USERNAME`/`MAVEN_CENTRAL_PASSWORD` from `~/.gradle/gradle.properties` (`mavenCentral.username`/`mavenCentral.password` — confirmed present on this machine; never print the values) and run, from `bindings/kotlin/`:\n   ```bash\n   ./gradlew clean \\\n     :publishReleasePublicationToMavenCentralRepository \\\n     :modules:runanywhere-core-llamacpp:publishReleasePublicationToMavenCentralRepository \\\n     :modules:runanywhere-core-onnx:publishReleasePublicationToMavenCentralRepository \\\n     -Prunanywhere.useLocalNatives=true -x buildLocalJniLibs --no-daemon\n   ```\n   GPG signing (`signing.gnupg.keyName`/`signing.gnupg.passphrase` in the same properties file, key id `CC377A9928C7BB18` — confirmed importable via `gpg --list-secret-keys --keyid-format LONG` on this machine) is read from the same properties file automatically; you do not pass it separately.\n5. Transfer to Central Portal:\n   ```bash\n   curl --fail --request POST -H \"Authorization: Bearer $CENTRAL_BEARER\" \\\n     'https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/io.github.sanchitmonga22?publishing_type=automatic'\n   ```\n   Then find the resulting `portal_deployment_id` — expect **exactly one** fresh repository; if the count isn't 1, stop and investigate rather than guessing which one is yours (see the doc for the exact `jq` filter).\n6. Poll `https://central.sonatype.com/api/v1/publisher/status?id=<id>` (POST, same bearer) until `deploymentState` is `PUBLISHED`. Automatic deployments normally go `PENDING -> VALIDATING -> PUBLISHING -> PUBLISHED` within a few minutes. **If it lands on `VALIDATED` instead of proceeding, that means Sonatype is waiting for a manual click in the Central Portal UI** — stop and tell the human; do not loop forever assuming it resolves itself. On `FAILED`, print `.errors` and stop.\n7. Verify propagation (allow 10-30 minutes for CDN propagation even after `PUBLISHED`):\n   ```bash\n   for a in runanywhere-sdk runanywhere-llamacpp runanywhere-onnx; do\n     echo \"$a: $(curl -s -o /dev/null -w '%{http_code}' \"https://repo1.maven.org/maven2/io/github/sanchitmonga22/$a/$VERSION/$a-$VERSION.pom\")\"\n   done\n   ```\n   Expect `200` for all three.\n\n## 3. pub.dev (Flutter)\n\n**Verify the current package set fresh — do not trust a hardcoded count.** As of this writing the real, non-QHexRT Flutter package set is **4**, not 3: `runanywhere`, `runanywhere_llamacpp`, `runanywhere_onnx`, and `runanywhere_mlx` (mlx was added after some earlier documentation/memory of \"3 packages\" was written — that assumption is stale; `runanywhere_mlx` is a real, already-published-on-pub.dev package, not new/WIP). Enumerate it yourself instead of trusting this list verbatim next time:\n```bash\nfind bindings/flutter/packages -maxdepth 1 -type d -not -iname '*qhexrt*' -not -path '*/packages'\n```\n`runanywhere_qhexrt` exists in that directory too — never publish it (same exclusion policy as Maven).\n\nStage the released iOS xcframeworks + Android `.so` into each package with the Flutter equivalent of `package-sdk.sh` (melos-based monorepo, confirmed at `bindings/flutter/scripts/package-sdk.sh`):\n\n```bash\nbash bindings/flutter/scripts/package-sdk.sh --mode local --natives-from <dir-with-xcframeworks-and-android-.so>\n```\n\nThis script already runs `flutter pub publish --dry-run` internally per package as part of its own validation — but run it again yourself, per package, from each package directory, and **read the output for real, not just the exit code**:\n\n```bash\ncd bindings/flutter/packages/runanywhere && flutter pub publish --dry-run\ncd ../runanywhere_llamacpp && flutter pub publish --dry-run\ncd ../runanywhere_onnx && flutter pub publish --dry-run\ncd ../runanywhere_mlx && flutter pub publish --dry-run\n```\n\nExpect zero warnings except a benign version-gap hint. Only then run the real publish, same order (core first, since the others depend on it):\n\n```bash\ncd bindings/flutter/packages/runanywhere && flutter pub publish\ncd ../runanywhere_llamacpp && flutter pub publish\ncd ../runanywhere_onnx && flutter pub publish\ncd ../runanywhere_mlx && flutter pub publish\n```\n\n**Credential note:** pub.dev OAuth is normally cached at `~/Library/Application Support/dart/pub-credentials.json` on macOS (confirmed present on this machine). If it's missing, expired, or `flutter pub publish` prompts for an interactive browser login, **STOP and tell the human** — you cannot complete a fresh Google OAuth flow non-interactively, and forcing/guessing here is not appropriate.\n\nVerify afterward against the live API (not a cached page):\n```bash\nfor p in runanywhere runanywhere_llamacpp runanywhere_onnx runanywhere_mlx; do\n  echo \"$p: $(curl -s \"https://pub.dev/api/packages/$p\" | python3 -c \"import json,sys;print(json.load(sys.stdin)['latest']['version'])\")\"\ndone\n```\n\n## 4. Swift SPM distribution repo (`RunanywhereAI/runanywhere-swift`)\n\nA separate, **generated-only** repo mirroring `bindings/swift` (Package.swift + Sources/ + LICENSE + README) so SwiftPM consumers using `from: \"<version>\"` don't clone the ~340MB monorepo.\n\n**GOTCHA:** this machine may have local directories literally named `runanywhere-swift` that are just checkouts of the **monorepo's** remote, not the distribution repo. Verify before trusting any existing local directory by name:\n```bash\ngit -C <candidate-dir> remote get-url origin\n```\nMust show `RunanywhereAI/runanywhere-swift`, not `runanywhere-sdks`. Always clone fresh instead of guessing:\n\n```bash\ngit clone https://github.com/RunanywhereAI/runanywhere-swift.git /tmp/ra-swift\n\nbindings/swift/scripts/sync-dist-repo.sh \\\n  --zips <exact-verified-Apple-archive-dir-from-the-release> --tag /tmp/ra-swift\n\nRUNANYWHERE_SWIFT_DIST_REPO=/tmp/ra-swift \\\n  bash scripts/validation/gates/check_swift_dist_repo_sync.sh\n\ngit -C /tmp/ra-swift push origin main --follow-tags\n```\n\nThe `--zips` dir must contain the verified (sha256-checked) release archives — `RACommons-ios-v$VERSION.zip`, `RABackend{LLAMACPP,MLX,NeuRT,ONNX,Sherpa}-ios-v$VERSION.zip`, `RunAnywhereMLX{Metal,Resources,Runtime}-ios-v$VERSION.zip` — downloaded and checked exactly per the general verification discipline, not re-used from an earlier, untrusted local copy.\n\n**The tag on `runanywhere-swift` is bare semver, no `v` prefix** (SwiftPM's `from:` needs that) — do not tag it `v$VERSION` by mistake; `sync-dist-repo.sh --tag` handles this correctly, but double check the pushed tag with `git -C /tmp/ra-swift tag --points-at HEAD` before walking away.\n\nThis is enforced, not just documented: once `v$VERSION` is tagged on `runanywhere-sdks`, `check_swift_dist_repo_sync.sh` fails every future PR on the monorepo until `runanywhere-swift` carries the matching bare-semver tag — so don't let this lag behind the other registries.\n\n**GATE GAP — `check_swift_dist_repo_sync.sh` does NOT catch dependency drift, only checksum/version-string drift.** `runanywhere-swift`'s `Package.swift` is manually maintained end to end — `sync-dist-repo.sh` only ever touches `sdkVersion` and the binaryTarget checksums, never the `dependencies:` block (mlx-swift, mlx-swift-lm, mlx-audio-swift, swift-crypto, etc.). If root `runanywhere-sdks/Package.swift` bumps one of those (e.g. a new fork tag), a human has to remember to mirror it into the dist repo by hand, and nothing fails CI if they forget. This exact drift shipped once (2026-08-17): the dist repo pointed `mlx-swift-lm` at unforked `ml-explore/mlx-swift-lm` instead of the `RunanywhereAI` fork, silently breaking every iOS/macOS MLX consumer of that tag until caught by an actual build failure ~40 minutes later. Before trusting a freshly-cut dist-repo tag: diff every `.package(url:` line between root `Package.swift` and the dist repo's `Package.swift` (`grep -n '\\.package(url:' Package.swift` on both sides), and actually build a real product that exercises the dependency you're worried about (e.g. `cd /tmp/ra-swift && swift build --product RunAnywhereMLX`) — don't rely on the sync gate alone. If you catch drift on a tag that's only minutes old with no evidence of external consumption, fixing it in place and force-moving the tag (`git tag -f $VERSION && git push origin $VERSION --force`) is the correct call, not a shortcut — a known-broken immutable tag helps no one.\n\n**If you force-move a tag, SwiftPM/Xcode consumers cache the OLD commit in several places — clearing just `Package.resolved` is not enough.** Verifying your own fix (or having a downstream consumer verify it) can silently keep failing even after the fix is correctly pushed, because: (1) any consumer repo's own tracked `*.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved` pins the exact old resolution and `xcodebuild -resolvePackageDependencies` respects it rather than re-resolving; (2) `~/Library/Caches/org.swift.swiftpm/repositories/<pkg>-<hash>/` is a global shared clone cache keyed by repo, oblivious to a moved tag; (3) `~/Library/org.swift.swiftpm/security/fingerprints/<pkg>-<hash>.json` records the fingerprint SwiftPM saw for that tag and can silently keep using it after a mismatch instead of failing loud. To actually re-resolve after moving a tag: delete the consumer's tracked `Package.resolved`, `rm -rf .build .swiftpm` and any `~/Library/Developer/Xcode/DerivedData/<Scheme>-*`, AND clear the matching entries under both global `org.swift.swiftpm` cache directories for every affected package identity — then resolve fresh.\n\n## 5. macOS rcli notarization\n\nCI (`native_rcli_macos` in `release.yml`) usually already produces a notarized, stapled artifact when Developer ID + notary secrets are configured in repo secrets. **Check the release assets first** before assuming you need to notarize anything yourself locally:\n\n```bash\ngh release view v$VERSION --json assets --jq '.assets[].name' | grep -iE 'rcli-macos|\\.dmg'\n```\nBaseline expectation is `rcli-macos-arm64-v$VERSION.tar.gz` (+ `.sha256`); a `.dmg` only appears when `RCLI_MACOS_FULL_RELEASE=1` was set for that build (see below). If a plain tarball is already present and its contents pass the general verification discipline, there is usually nothing further to do here.\n\n**If you do need to notarize locally**, everything lives in `rcli/scripts/package-rcli.sh` (read its header comment — it documents its own contract in detail):\n\n- One-time setup, check before redoing: `xcrun notarytool history --keychain-profile runanywhere-notary` (confirmed configured on this machine — a fresh submission history returns without error). If missing: `xcrun notarytool store-credentials \"runanywhere-notary\" --apple-id <email> --team-id <team> --password <app-specific-password>`.\n- Codesign identity: `security find-identity -v -p codesigning | grep \"Developer ID Application\"` — confirmed present on this machine as `\"Developer ID Application: RunAnywhere, Inc (<TEAM_ID>)\"`. Grep for it fresh rather than hardcoding the team-id suffix, it can rotate.\n- Invocation contract (env vars, from the script header): `RCLI_CODESIGN_IDENTITY`, `RCLI_MACOS_NOTARIZE=1`, `RCLI_MACOS_FULL_RELEASE=1`, `RCLI_MACOS_SWIFT_BIN_DIR=<output of build-mlx-cli.sh>`. `RCLI_MACOS_FULL_RELEASE=1` is what additionally stages `mlx.metallib` and SwiftPM resource bundles and is what causes a notarized, stapled `.dmg` to be emitted alongside the Homebrew-style tarball. `RCLI_CODESIGN_KEYCHAIN` / `RCLI_NOTARYTOOL_KEYCHAIN` matter only if the identity/profile live in a non-default keychain.\n- Call shape: `rcli/scripts/package-rcli.sh <build-dir> macos-arm64` with the env vars above exported.\n\n## 6. Electron — build AND packaging are now in `release.yml`; publish is still manual\n\n**Reversing the earlier \"deliberately separate\" note**: as of `fix/electron-cicd-staging-url-npu-ane` (2026-08-21), the repo owner asked for Electron to produce ready-to-publish artifacts through the real release train like every other SDK, so `release.yml` now has `native_electron` (invokes `.github/workflows/electron-native-package.yml` as a reusable `workflow_call`) and `sdk_electron` (downloads its `electron-packaged-tarballs` artifact, runs `scripts/release/prepublish_check.py` on every tarball, checksums them, and splits public vs. QHexRT-private). `publish` hard-gates on `sdk_electron` succeeding. `@runanywhere/electron`, `-llamacpp`, `-onnx`, `-sherpa`, `-neurt` land as real, `.sha256`-sidecarred GitHub Release assets, same as the Web/RN npm tarballs. `@runanywhere/electron-qhexrt` still never becomes a public Release asset (policy below) — it's produced and packaged every release, but only reachable as the `sdk-electron-qhexrt-private` workflow artifact on that run, for a human to `npm publish` by hand. Electron is **not** WIP on a branch anymore: the old root `AGENTS.md` \"Active issues\" section describing `smonga/electron_upgrade` as in-progress is gone entirely as of this checkout — `bindings/electron/` is a fully merged, real SDK on `main`. Re-check that section is still absent before trusting this note.\n\n`.github/workflows/electron-native-package.yml` itself is unchanged in what it builds: every backend for real (llamacpp/onnx/sherpa/neurt on their real platforms, qhexrt on a self-hosted Windows ARM64 runner) with the real `STAGING_BASE_URL` baked in, gated by `check_plugin_natives.py` (which fails closed on the staging-URL placeholder — the exact bug that shipped through `0.20.24`). What's new is that it's also invocable via `workflow_call`, and that `native_win_x64_and_package` now tolerates a failed/skipped QHexRT lane (self-hosted, single machine) rather than losing the other five real packages to one runner's outage. **`release.yml` itself still does not `npm publish` anything** — it only produces artifacts (Release assets for the public five, a workflow artifact for QHexRT) — same \"build automated, publish manual\" shape as every other platform. See `thoughts/shared/plans/electron_cicd_staging_url_and_npu_ane_pipeline.md` for the full history.\n\n**PROVEN END TO END on v0.20.25 (2026-08-22)** — the first release where this actually ran. Final state: 65 release assets, all 5 public Electron tarballs present with `.sha256` sidecars, zero `qhexrt`-named assets. Verified by downloading the *published* tarballs and running the repo's own gate against them:\n\n```bash\ngh release download v<version> --repo RunanywhereAI/runanywhere-sdks -p 'runanywhere-electron*.tgz' -D /tmp/verify\npython3 scripts/validation/gates/check_plugin_natives.py --expect-version <version> \\\n    $(for t in /tmp/verify/*.tgz; do printf -- '--tarball %s ' \"$t\"; done)\n# => All 20 check(s) passed: plugin natives are routable and no staging-URL placeholder was found.\n```\n\nThat single command asserts all three things you actually care about, per binary: the real `STAGING_BASE_URL` is baked in (no `YOUR_STAGING_BASE_URL`), `rac_commons` embeds the right version, and every backend is genuinely **routable** (`g_llamacpp_ops`, `g_sherpa_stt_ops`, `g_neurt_llm_ops`, `g_onnx_embeddings_ops`, …) rather than a same-sized shell. **Run it before every Electron `npm publish`** — it is the cheapest defense against exactly the class of defect that shipped `electron-sherpa` 0.20.17's NULL-vtable carrier. Expected shipped layout: entry package = `darwin-arm64` + `win32-x64` + `win32-arm64`; llamacpp/onnx/sherpa = `darwin-arm64` + `win32-x64`; neurt = `darwin-arm64` only; qhexrt = `win32-arm64` only.\n\nWhere to get the QHexRT tarball for its manual publish (it is deliberately NOT a release asset):\n\n```bash\ngh run download <release-run-id> --repo RunanywhereAI/runanywhere-sdks \\\n    --name sdk-electron-qhexrt-private --dir /tmp/qhexrt\n```\n\nIf asked to actually publish an Electron package (still a real, separate, manual step — this skill's general discipline in §0/§General verification still applies in full):\n- Involve the Windows box for real Windows-native verification before publishing anything Windows-targeted. The team keeps a **Windows ARM64 machine reachable over SSH**; its hostname, user, key, and mesh-VPN node name are operator-specific and deliberately not recorded in this repo — read the Windows/ARM64 host entry out of the operator's `~/.ssh/config` and use that alias. If there is no such entry, the box is not configured for you: say so and ask, rather than guessing at a hostname. Check reachability **first** by asking the mesh-VPN client whether the node is up (e.g. `tailscale status`, matching on the host entry's `HostName`) — if it reports `offline, last seen ...`, the box is asleep, which is not a configuration problem: ask the human to wake it, do not debug it as broken. Only then `ssh -o ConnectTimeout=8 \"$WIN_BOX\" \"whoami\"`. That same machine is **also** registered as a GitHub Actions self-hosted runner (labels `[self-hosted, Windows, ARM64, qhexrt]`) running as a Windows service — a queued `native_win_arm64_qhexrt` job on it means the machine is busy; check `gh api repos/RunanywhereAI/runanywhere-sdks/actions/runners` for the matching runner's `busy`/`status` before assuming it's free for manual SSH work, and vice versa.\n- **`@runanywhere/electron-qhexrt` being public on npm is a CONFIRMED, settled policy decision** (repo owner, 2026-08-20) — it contains real Hexagon NPU binaries (`libQnnHtpV81Skel.so`, `QnnHtp*.dll`, `rac_commons.dll`, `runanywhere_qhexrt.dll`) on purpose. The QHexRT-stays-private policy enforced everywhere else in this skill is specifically about the **GitHub-Release-side** asset set (Android etc.), a different channel — not a contradiction. Stop treating this as an unresolved item to flag; only re-raise it if you find evidence the policy changed.\n\n## 7. Final sanity pass\n\nAfter every registry is done, re-view each package's public listing/API and compare against the previous version — a big unexplained drop in file count or package size is a red flag to investigate before calling the release fully published:\n\n```bash\n# npm\nfor p in proto-ts web web-llamacpp web-onnx core llamacpp mlx onnx; do\n  npm pack @runanywhere/$p@$VERSION --dry-run 2>&1 | tail -5\ndone\n\n# Maven — check module metadata / POM sizes are non-trivial, not near-empty\nfor a in runanywhere-sdk runanywhere-llamacpp runanywhere-onnx; do\n  curl -s -o /dev/null -w \"%{size_download} bytes  $a\\n\" \\\n    \"https://repo1.maven.org/maven2/io/github/sanchitmonga22/$a/$VERSION/$a-$VERSION.aar\"\ndone\n\n# pub.dev\nfor p in runanywhere runanywhere_llamacpp runanywhere_onnx runanywhere_mlx; do\n  curl -s \"https://pub.dev/api/packages/$p\" | python3 -c \"import json,sys;d=json.load(sys.stdin);print('$p', d['latest']['version'])\"\ndone\n```\n\nOnly after this pass — every registry shows `$VERSION`, every artifact's contents check out, and nothing regressed in size/file-count versus the previous release — is the release fully published. Do not skip this step just because each individual publish call reported success; a successful upload is not the same claim as \"the right bytes are now live.\"\n",".claude/skills/sdk-release/SKILL.md":"---\nname: sdk-release\ndescription: Cut a new RunAnywhere SDK release end-to-end in RunanywhereAI/runanywhere-sdks — version bump through a fully green, tagged, drafted-then-published GitHub Release. Stops BEFORE publishing to package registries (npm/Maven/pub.dev/SwiftPM dist) — that is the separate \"sdk-publish\" skill, invoked as the last step here.\n---\n\n# SDK Release (runanywhere-sdks)\n\nRepo: `~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks` (verify with `pwd`/`ls` —\nthis is a workspace subfolder, not the repo root of the whole Qualcomm/ tree, and paths\ndrift; re-check script paths below still exist before trusting them blindly).\n\nThis is a **runbook**, not a description. Follow the steps in order. Every `gh`/`git`\ncommand below is meant to be run close to verbatim — adjust only the version/PR/run-id\nvalues for the release you're actually cutting.\n\n**THIS SKILL HAS BEEN SKIPPED THREE TIMES IN A ROW (v0.20.29, v0.20.30, v0.20.31),\nproducing the IDENTICAL failure each time, DESPITE step 2/3 below documenting the exact\nfix since the v0.20.25/v0.20.29 incidents.** All three times, an agent ran\n`sync-versions.sh`, opened the PR, merged it, and pushed the tag directly — skipping the\n\"dispatch a release candidate BEFORE merging, sync checksums from it, THEN merge\" sequence\nin steps 2-3 — usually because the version bump was one step inside a longer autonomous\nchain (a downstream repo re-pin, a multi-repo release cascade) and felt like \"just a\nversion bump,\" not a release. **A version bump to `core/VERSION` IS a release. If you are\nabout to run `scripts/release/sync-versions.sh` for ANY reason — including re-pinning this\nrepo as a step in some OTHER repo's release — stop and follow this skill from step 1,\nincluding the candidate dispatch. Do not improvise the sequence from memory even if you\ndid it correctly last time; do not skip it because \"this bump is small\" or \"we're in a\nhurry.\"** v0.20.31 additionally caught a second, previously-undetected instance of the\nsame class of bug in the SAME commit: `RunAnywhereMLXRuntime` in the Flutter podspec (which\n`RAC_CHECKSUM_SKIP` exempts from CI verification, so it fails silently for a consumer\ninstead of failing `publish`) was ALSO stale, and had to be independently re-verified\nagainst the same candidate build — see step 3's checksum-location table; check every row\nin it, not just the one CI complained about.\n\nCompanion skill: **sdk-publish** — invoke it at the very end (step 10) for the actual\nnpm / Maven / pub.dev / SwiftPM-distribution publishing. This skill's job stops at a\npublished (non-draft) GitHub Release with correct assets.\n\n---\n\n## 0. Orientation facts (confirm these still hold before relying on them)\n\n- Canonical version file: `core/VERSION` (single line, e.g. `0.20.24`). `core/VERSIONS`\n  also carries a `PROJECT_VERSION=` line that must agree — `sync-versions.sh` updates\n  both, `auto-tag.yml` reads `core/VERSIONS`, `check_release_version_coherence.sh` reads\n  `core/VERSION`. Don't hand-edit either; always go through the script.\n- `scripts/release/sync-versions.sh <version>` — bumps ~20 files (Package.swift,\n  gradle.properties, all package.json/pubspec.yaml, Kotlin/Swift/TS version constants,\n  AGENTS.md). Accepts a leading `v` and strips it.\n- `scripts/validation/gates/check_release_version_coherence.sh` — the gate that enforces\n  \"PR changing VERSION must carry exactly one `release:patch|minor|major` label, and the\n  new version must be exactly base+1 for that bump type.\" Reads `PR_BASE_SHA` and\n  `PR_RELEASE_LABELS_JSON` env vars when run under CI; locally it only checks the\n  canonical-version-format part.\n- `.github/workflows/pr-build.yml` job **`centralization`** is what actually runs the\n  coherence gate in CI (`Release-train version coherence` step), alongside\n  `check_typescript_centralization.sh`, `check_flutter_centralization.sh`,\n  `check_gradle_centralization.sh`, `check_wasm_provenance_contract.sh`,\n  `check_swift_dist_repo_sync.sh`, `check_agents_claude_sync.sh`,\n  `check_no_hardcoded_defaults.sh`. This job's trigger types are\n  `[opened, synchronize, reopened, labeled, unlabeled]` — **adding the release label\n  after opening the PR fires a fresh run**, which is how the gate clears without a new\n  commit (see step 1).\n- `.github/workflows/release.yml` — the build/package/draft-release workflow. Triggers:\n  tag push `v*.*.*` OR `workflow_dispatch` (works on **any branch**, not just tags) with\n  inputs `version` (required), `validate_external_starters` (bool, default false),\n  `publish_from_run_id` (optional), `reuse_native_web_run_id` (optional).\n- `.github/workflows/auto-tag.yml` — fires on `pull_request: closed` to `main` when the\n  merged PR carries a `release:*` label. Verifies the version bump matches the label,\n  pushes the `v<version>` tag, and **explicitly dispatches a fresh `release.yml` run at\n  that tag** (plain `git push` of a tag with `GITHUB_TOKEN` does not auto-trigger\n  workflows, so it calls `gh workflow run release.yml --ref v<version> -f version=<version>`\n  itself). This is the redundant rebuild you must cancel — see step 7.\n- Branch protection on `main`: check\n  `gh api repos/RunanywhereAI/runanywhere-sdks/branches/main/protection` — 2 approvals +\n  code owner review required, but look for a `bypass_pull_request_allowances` (users/teams/apps)\n  list. If your `gh api user --jq .login` is on that list, `gh pr merge --squash --admin`\n  is a legitimate, pre-configured bypass — not a hack. If not on the list, stop and ask a\n  human to approve/merge.\n- Never let any `qhexrt`-named asset reach the public asset set. `release.yml`'s own\n  manifest-assertion step (`find release-flat -maxdepth 1 -type f -iname '*qhexrt*'`)\n  hard-fails the run if one is found — this is enforced, not just policy.\n- **Electron IS part of `release.yml` now** (as of `fix/electron-cicd-staging-url-npu-ane`,\n  2026-08-21) — this reverses the prior \"deliberately separate\" policy noted below by an\n  earlier pass; the repo owner explicitly asked for Electron to produce ready-to-publish\n  artifacts through the real release train like every other SDK. `native_electron`\n  invokes `.github/workflows/electron-native-package.yml` as a **reusable workflow**\n  (`workflow_call`) rather than duplicating its ~400 lines of hard-won native-build logic\n  inline; `sdk_electron` downloads the resulting `electron-packaged-tarballs` artifact,\n  runs `scripts/release/prepublish_check.py` on every tarball, computes checksums, and\n  splits public vs. QHexRT-private before anything reaches `release-flat`. `publish`\n  hard-gates on `sdk_electron` succeeding (same tier as `sdk_kotlin`/`sdk_web`/\n  `sdk_react_native`) but treats `native_electron`'s own result as advisory, because that\n  job's result folds in `native_win_arm64_qhexrt` — a single, personally-owned\n  self-hosted NPU runner whose outage should cost only `@runanywhere/electron-qhexrt`\n  for that release, not the whole SDK train (`native_win_x64_and_package`, the job that\n  actually produces the tarballs, tolerates a failed/skipped QHexRT lane and still packages\n  the other five real artifacts). Like every other SDK here, this still does **not**\n  `npm publish` anything — it only produces the tarballs as GitHub Release assets\n  (public ones) / a workflow artifact (the private QHexRT one) for a human to publish by\n  hand, matching the \"build automated, publish manual\" shape everywhere else.\n  **Electron is no longer WIP on a branch** — the old root `AGENTS.md`/`CLAUDE.md`\n  \"Active issues\" section describing `smonga/electron_upgrade` as in-progress is gone\n  entirely as of this checkout; `bindings/electron/` is a fully merged, real SDK on\n  `main` with its own `AGENTS.md`. Re-check that section is still absent before trusting\n  this note — if it has reappeared, something regressed.\n  See `thoughts/shared/plans/electron_cicd_staging_url_and_npu_ane_pipeline.md` for the\n  full history (this pipeline shipped the placeholder-URL bug three times before\n  `electron-native-package.yml` existed).\n  **QHexRT public/private boundary — CONFIRMED POLICY, not an open flag**: the repo\n  owner explicitly confirmed (2026-08-20) that `@runanywhere/electron-qhexrt` stays\n  public on npm intentionally (it already is, and is a real, working precedent worth\n  studying before touching QHexRT packaging elsewhere) — the QHexRT-stays-private\n  policy enforced everywhere else in this pipeline is specifically about the\n  **GitHub-Release-side** asset set (e.g. Android), a different distribution channel.\n  This is settled; stop re-flagging it as an unresolved contradiction. Only re-raise it\n  if you find evidence the policy has changed.\n\n---\n\n## 0b. NeuRT is a PINNED PREBUILT now — check this before anything else\n\n`native_ios` no longer compiles the private `neurun` repo. It downloads archives that\nrepo publishes, pinned by tag + SHA-256 in `core/VERSIONS`, the same shape as sherpa-onnx:\n\n```\nNEURUN_REPO=RunanywhereAI/neurun      # shared: one repo publishes BOTH engines\nNEURT_RELEASE_TAG=v<version>          # per-engine: a release may carry one lane only\nNEURT_RAC_ABI_VERSION=9\nNEURT_MACOS_ARM64_SHA256=…\nNEURT_IOS_ARM64_SHA256=…\nNEURT_IOS_ARM64_SIMULATOR_SHA256=…\n```\n\n`NEURUN_REPO` is shared because one neurun release carries both engines. The **tags stay\nper-engine** on purpose: that repo can cut a release for one lane only, so a given tag may\ncarry just one engine's assets.\n\n**Run the gate after re-pinning** — it verifies the pins against the release itself, so a\nmistyped or locally-computed value fails there instead of mid-build:\n\n```bash\nNEURUN_TOKEN=\"$(gh auth token)\" bash scripts/validation/gates/check_engine_prebuilt_pins.sh\n```\n\n### Local engine development — there is no source mode\n\nThe engine has no `NEURT_ROOT` path any more; nothing here compiles engine source. To iterate\non NeuRT, build a slice in the neurun checkout and point the engine at it — no release, no\ntag, no pin edit:\n\n```bash\n# in neurun\nNeuRT/tools/scripts/package-rac-dist.sh --sdk-root <this repo> --version 0.0.0-dev \\\n    --slice macos-arm64 --out /tmp/devdist\nmkdir -p /tmp/devslice && tar -xzf /tmp/devdist/neurt-macos-arm64-v0.0.0-dev.tar.gz \\\n    -C /tmp/devslice --strip-components=1\n\n# here\nNEURT_PREBUILT_ROOT=/tmp/devslice cmake -S . -B build/dev -G Ninja \\\n    -DRAC_BUILD_BACKENDS=ON -DRAC_BACKEND_NEURT=ON -DRAC_RUNTIME_COREML=ON\n# expect: Mode: PREBUILT (macos-arm64, ABI 9)\n```\n\n`NEURT_PREBUILT_ROOT` is a **single-slice** root, which is why\n`build-core-xcframework.sh` deliberately ignores it: packaging needs all three slices, so\nhonouring a single-slice override there would let one staged slice satisfy the guard and\nsilently package two non-routable stubs. Use an obviously-not-a-release version like\n`0.0.0-dev` so a dev slice can never be mistaken for a published one.\n\n**Why this changed, and why it matters to you specifically.** `engines/neurt/CMakeLists.txt`\nused to resolve a `NEURT_ROOT` checkout of that private repo and **regex-parse its root\n`CMakeLists.txt`** for a source list. Under that arrangement, commons widened\n`rac_llm_stream_callback_fn` with `tokens_in_delta`, the pinned neurun commit still passed\n4 arguments, and `native_ios` died with *\"too few arguments to function call, expected 5,\nhave 4\"* on **v0.20.15, v0.20.16 and v0.20.17**. Every packaging job needs `native_ios`,\nso all three shipped **iOS-only — 18 assets where a healthy release has 55+**. That is the\nsingle most expensive failure this runbook exists to prevent, and it came from this seam.\n\n**Before opening the release PR**, confirm the pin is current:\n\n```bash\ngrep -A5 '^NEURT_REPO' core/VERSIONS\ngh release view \"$(grep '^NEURT_RELEASE_TAG=' core/VERSIONS | cut -d= -f2)\" \\\n    --repo RunanywhereAI/neurun --json assets --jq '.assets[].name'\n```\n\nExpect six assets — three `neurt-<slice>-v<version>.tar.gz` plus their `.sha256` sidecars.\n\n**If the plugin ABI changed this cycle** (`RAC_PLUGIN_API_VERSION` in\n`core/include/rac/plugin/rac_plugin_entry.h`), the pinned archives are invalid and\n`native_ios` will fail at the download step. Cut a neurun release first — that repo's\n`release_sdk_archives` skill is the runbook — then re-pin both `NEURT_RELEASE_TAG` and\n`NEURT_RAC_ABI_VERSION` here. The downloader refuses to run if `NEURT_RAC_ABI_VERSION`\ndisagrees with the header, so this cannot be half-done.\n\nProve it locally before trusting a tag build:\n\n```bash\nNEURUN_TOKEN=\"$(gh auth token)\" bash scripts/build/download-neurt.sh --all\n```\n\nThat verifies each tarball's SHA-256 **and** that its `RECEIPT.json` names the same ABI\nversion this repo defines. Then check configure prints `Mode: PREBUILT (<slice>, ABI N)`.\n`SOURCE` means a local neurun checkout was picked up (prebuilt is meant to win, so the\ndownload did not land); `SHELL` means the engine is **not routable** and a release built\nnow would ship an engine that registers, claims Apple availability and serves nothing.\n\n`release.yml` treats the downloader's exit **3** (\"no token — degrade to the shell\") as\n**fatal**. That degrade is right for a fork PR and catastrophic for a release.\n\n### The complete engine matrix — who consumes what, and how\n\nThere are only **three fetch points in the entire repo**. Everything else consumes an\nsdks artifact, which is why a single fetch fix can repair five SDKs at once — and why a\nsingle missing fetch can silently break several.\n\n```\nneurun release (private)\n  ├─ native_ios         ──NeuRT x3 slices──►  RABackendNeuRT.xcframework\n  │                                              ├─ Swift  (Package.swift binary target)\n  │                                              ├─ Flutter / React Native (co-vendored\n  │                                              │   in the onnx package's podspec)\n  │                                              └─ rcli-macos (stages it, then GREPS\n  │                                                  `backends --json` for neurt)\n  ├─ native_android     ──QHexRT arm64-v8a──►  one build stages jniLibs into\n  │                                              Kotlin + React Native + Flutter\n  └─ electron           ──NeuRT macos-arm64 AND QHexRT win-arm64──►  its own natives\n```\n\n| target | NeuRT | QHexRT | note |\n|---|---|---|---|\n| Swift (iOS/macOS) | ✅ xcframework | — | NeuRT is Apple-only |\n| Flutter / React Native (Apple) | ✅ xcframework | — | co-vendored in the onnx package |\n| Flutter / React Native (Android) | — | ✅ jniLibs | from the same Android build as Kotlin |\n| Kotlin (Android) | — | ✅ jniLibs | arm64-v8a only; the only ABI with a Hexagon NPU |\n| Electron macOS | ✅ fetches | — | builds its own natives |\n| Electron win-arm64 | — | ✅ fetches | |\n| rcli macOS | ✅ xcframework | — | **best check in the tree** (below) |\n| rcli linux / windows-x64 | — | — | correct: NeuRT is Apple-only, QHexRT is win-arm64 |\n\n**`rcli-macos` is the strongest verification that exists here.** It greps\n`backends --json` for `\"name\":\"neurt\"` — and a non-routable shell *refuses\nregistration*, so that grep proves the engine is **routable**, not merely present. Copy\nthis pattern when adding an engine rather than checking for a file on disk.\n\n**Two silent-failure traps this matrix exists to prevent:**\n\n- **`RAC_BACKEND_<X>=ON` does not mean the engine works.** Without a payload the engine\n  builds as a non-routable shell and registration is refused — no build error. Android\n  shipped that way for months, and `rcli-macos-release` still sets\n  `RAC_BACKEND_NEURT: ON` regardless of whether a payload was fetched.\n- **`copy_if_exists` in `build-core-android.sh` silently no-ops** when a lib was never\n  built. That is the same guarded-copy pattern that shipped Windows with zero engines for\n  several releases. The release now asserts `librunanywhere_qhexrt.so` is really inside\n  the arm64-v8a archive.\n\n`scripts/validation/gates/check_engine_prebuilt_pins.sh` asserts all of the above plus\nthe pins, the ABI, header drift against the receipt, and (with a token) the release\ncontents. Run it before cutting a release; it is also wired into `pr-build` offline.\n\n### QHexRT is pinned the same way — check it too\n\nThe Hexagon-NPU engine crosses the identical boundary, published by the same neurun release:\n\n```\nQHEXRT_RELEASE_TAG=v<version>\nQHEXRT_ARM64_V8A_SHA256=…      QHEXRT_ARM64_V8A_RECEIPT=…\nQHEXRT_WIN_ARM64_SHA256=…      QHEXRT_WIN_ARM64_RECEIPT=…\n```\n\nTwo pins per ABI, not one: the tarball checksum **and** the payload's own build-receipt\nhash. The receipt is the directory name under `engines/qhexrt/prebuilt/versions/`, so\npinning it is what makes the pin describe the bytes rather than trusting the archive's\nown claim about itself.\n\n```bash\nNEURUN_TOKEN=\"$(gh auth token)\" bash scripts/build/download-qhexrt.sh --abi arm64-v8a\nNEURUN_TOKEN=\"$(gh auth token)\" bash scripts/build/download-qhexrt.sh --abi win-arm64\n```\n\n**`current` selects ONE ABI at a time.** Both payloads can sit in `versions/` together, but\na build targets one platform — so re-run the downloader when switching between an Android\nand a Windows build. The downloader ends by running `validate-qhexrt-prebuilt.py` itself,\nso a bad payload fails at the pin rather than mid-build.\n\n**The QHexRT lane in neurun is deliberately non-blocking.** It runs on a single\nself-hosted Snapdragon box (QAIRT is licence-gated and exists on no hosted runner). If\nthat machine is offline, neurun's release publishes the Apple artifacts and **warns** that\nno QHexRT payload was produced — and the SDK simply keeps its previously pinned prebuilt.\nThat is correct behaviour, not a failure: do not block an SDK release on it. Only treat it\nas blocking if this release is specifically meant to ship new NPU kernels.\n\n**Do not hand-stage `engines/qhexrt/prebuilt/`.** That was the old flow, and it is how the\nElectron win-arm64 lane once pinned a receipt staged months earlier and silently shipped\nNPU kernels from an old commit. If the tree looks stale, re-pin and re-download; never\ncopy a payload in by hand.\n\n---\n\n## 1. Decide the version and open the release PR\n\nCheck for an already-open release PR first — don't duplicate work:\n\n```bash\ngh pr list --repo RunanywhereAI/runanywhere-sdks --label release:patch --label release:minor\n```\n\n(gh ORs multiple `--label` flags into a single AND-filtered query per invocation is not\nguaranteed across all gh versions — if unsure, run the two label queries separately:\n`--label release:patch` then `--label release:minor`, plus `--label release:major`.)\n\nPick a patch/minor/major bump based on the changes since the last tag. Then:\n\n```bash\ngit checkout -b release/v<version>\n./scripts/release/sync-versions.sh <version>      # e.g. 0.20.25 (no 'v' needed, either works)\nbash scripts/validation/gates/check_release_version_coherence.sh\n```\n\nThe coherence script may point out generated locks/changelogs that need regenerating\n(e.g. package-lock.json version bumps it expects but didn't find, or a missing CHANGELOG\nheading for the new version). Fix everything it flags, and add **explicit, human-written\nchangelog entries** — the gate requires a heading for the new version to exist; it does\nnot write prose for you.\n\nOpen the PR and **apply exactly one `release:patch` or `release:minor` (or `release:major`)\nlabel** — this is required, not optional. Two failure modes to know:\n\n- If you forget the label and push a commit later, the `centralization` job's\n  `Release-train version coherence` step fails.\n- **Adding the label after the PR is already open does NOT get picked up by the run that\n  already executed.** Because `pr-build.yml`'s `centralization` job triggers on\n  `labeled`/`unlabeled` PR events, applying the label fires a **new** run of the whole\n  job — that new run is what goes green. Don't push an empty commit to \"retrigger\" it;\n  just add/re-add the label (or use `gh pr edit <pr> --add-label release:patch`, and if it\n  was already present, remove + re-add to force the event).\n\n---\n\n## 2. Get REAL native-artifact checksums BEFORE merging — dispatch a release candidate\n\nDo not merge on faith that native builds will succeed. `release.yml`'s\n`workflow_dispatch` trigger works on **any branch**, including your unmerged release\nbranch — use it to build a full release candidate first:\n\n```bash\ngh workflow run release.yml --ref release/v<version> -f version=<version>\n```\n\nGet the run id and watch it:\n\n```bash\ngh run list --repo RunanywhereAI/runanywhere-sdks --workflow=release.yml --limit 5\n```\n\n**Do not block synchronously waiting on it.** Load the `Monitor` tool (or a background\npoll loop) to watch for completion/failure, and keep doing other release prep (changelog\nreview, CodeRabbit thread triage, branch-protection check) in the meantime. Keep\nnotifications to real state changes only — don't ping on every intermediate job\ncompleting, only on the run's overall failure or full completion.\n\n### REAL job durations — measured, not guessed. NEVER cancel a job for being \"slow\".\n\nMeasured on the 0.20.25 train (2026-08-21/22). A release train legitimately takes\n**4-5 hours** wall-clock when `native_web` has to build.\n\n| Job | Real duration | Notes |\n|---|---|---|\n| **`native_web`** (release.yml) | **~3h51m** | The long pole, by far. Its `Vendor ONNX Runtime and Sherpa-ONNX WASM archives` step alone is **~71 minutes** — it BUILDS onnxruntime + sherpa to WASM from source; it is not a git clone. Then 4 more WASM builds: CPU core+llamacpp+onnx ~89m, WebGPU llamacpp ~37m, WebGPU onnx+sherpa ~33m. |\n| `native_ios` | ~60-75 min | XCFrameworks + MLX. No longer compiles neurun — it downloads the pinned NeuRT archives (see §0b), which is seconds, not minutes. |\n| `swift-spm` (pr-build) | **69-85 min** | `Build XCFramework` step is most of it. Measured across 6 consecutive `main` runs (2026-08-21/22): 69, 78, 75, 85, 77, 69. A run sitting at ~78 minutes is **healthy**, not hung — check this row before reaching for cancel. |\n| `kotlin-android` (pr-build) | ~40 min | |\n| `python-windows` (pr-build) | ~40 min | |\n| `electron (windows-2022)` (electron-sdk-ci) | ~40 min | |\n| `rcli-windows` | ~30 min | |\n| `native_electron / native_win_arm64_qhexrt` | ~28 min | Self-hosted; may sit **queued** for a long time first if the single runner is busy. Queued ≠ hung. |\n| `native_electron / native_win_x64_and_package` | ~35-45 min | |\n| `wasm` (pr-build) | ~12 min | **Do NOT use this as a proxy for `native_web`.** Totally different scope. |\n\n**The mistake this table exists to prevent**: `pr-build.yml`'s `wasm` job takes ~12\nminutes, so a ~50-minute `native_web` \"vendor\" step looks hung. It is not. On the 0.20.25\ntrain this misread caused two healthy `native_web` jobs to be cancelled at 2h19m and\n~55m, wasting ~3 hours and two full candidate builds. Before concluding \"hung\", check\nstep-level progress and compare against THIS table:\n\n```bash\ngh run view <run-id> --repo RunanywhereAI/runanywhere-sdks --json jobs \\\n    --jq '.jobs[] | select(.name==\"native_web\") | {startedAt, steps: [.steps[] | select(.status==\"in_progress\") | .name]}'\n```\n\n### `reuse_native_web_run_id` is the correct way to avoid re-paying that ~4 hours\n\nIf a run needs redoing and **nothing under `bindings/web/wasm/`, `engines/`, or `core/`\nchanged** since a run whose `native_web` succeeded, reuse it instead of rebuilding:\n\n```bash\ngit diff --stat <good-run-sha> <current-sha>      # prove the scope first\ngh workflow run release.yml --ref release/<version> \\\n    -f version=<version> -f reuse_native_web_run_id=<good-run-id>\n```\n\n`native_web` then shows `skipped`, and `sdk_web` + `validate_consumer_web` still run and\npass against the reused artifact — which is also your proof the reuse was valid. Track the\nrun id that **originally built** it: that is the id you keep citing (see the chaining note\nin step 7).\n\n---\n\n## 3. Sync checksums once native_ios finishes\n\nPre-tag, `gh release download` will not work (there is no release yet) — pull the run's\nartifact directly:\n\n```bash\ngh run download <candidate-run-id> --repo RunanywhereAI/runanywhere-sdks \\\n    --name native-ios-macos --dir release-artifacts/native-ios-macos\n```\n\nVerify every `.zip` against its `.sha256` sidecar **yourself** before trusting it:\n\n```bash\ncd release-artifacts/native-ios-macos\nfor f in *.zip; do shasum -a 256 -c \"${f}.sha256\" || echo \"MISMATCH: $f\"; done\ncd -\n```\n\nThen sync:\n\n```bash\nbindings/swift/scripts/sync-checksums.sh release-artifacts/native-ios-macos\n```\n\n**KNOWN FACT — do not treat this as a bug**: `RunAnywhereMLXRuntime` (and occasionally\n`RACommons`) are **not byte-reproducible** between rebuilds of identical source. Expect\ntheir checksums to differ from any prior candidate build every single time you rebuild,\neven with zero source changes. Only investigate if a checksum for a framework that\nhistorically IS reproducible (e.g. plain XCFrameworks with no Swift codegen step)\nsuddenly changes.\n\n**Where each checksum actually lives — these are NOT all in one file:**\n\n| Manifest | Carries |\n|---|---|\n| root `Package.swift` | `RACommons` + the 5 backend XCFrameworks (6 checksums total) |\n| nested Flutter `Package.swift` (under `bindings/flutter/`) | same 6 |\n| `bindings/flutter/packages/runanywhere_mlx/ios/runanywhere_mlx.podspec` → `mlx_checksums` hash | `RunAnywhereMLXRuntime`, `RunAnywhereMLXMetal`, `RunAnywhereMLXResources` — **only here**, nowhere else |\n\nAfter `sync-checksums.sh` runs, verify centralization didn't drift and re-run the version\ncoherence gate locally:\n\n```bash\nbash scripts/validation/gates/check_flutter_centralization.sh\nbash scripts/validation/gates/check_release_version_coherence.sh\n```\n\nCommit and push the checksum-sync commit to the release branch:\n\n```bash\ngit add Package.swift bindings/flutter/Package.swift \\\n    bindings/flutter/packages/runanywhere_mlx/ios/runanywhere_mlx.podspec\ngit commit -m \"release: sync v<version> checksums from candidate run <candidate-run-id>\"\ngit push\n```\n\n### THIS STEP IS NOT OPTIONAL — skipping it fails `publish` after the tag already exists\n\n**What happens if you skip it** (observed on the 0.20.25 train): every build and package\njob goes green, `publish` gets all the way past flatten and the asset-manifest assertion,\nthen dies at **`Verify built Apple checksums match tagged package contracts`**:\n\n```\nmismatch: RACommonsBinary\n  Package.swift: 0609dae6...      release zip: 1ae55ae4...\nmismatch: RABackendLlamaCPPBinary\n  Package.swift: 0597442f...      release zip: 7062f7c8...\n>> Done. 8 processed, 0 missing, 4 failed.\nERROR: built Swift archives do not match the immutable tagged manifest\n```\n\nNote `8 processed / 4 failed` is really **2 distinct archives counted twice** (root\nmanifest + Flutter manifest each). On that train `RACommons` and `RABackendLLAMACPP`\ndrifted while ONNX/Sherpa/NeuRT/MLX/MLXMetal/MLXResources all matched — so \"6 of 8 passed\"\nis a normal-looking partial failure, not evidence of something exotic.\n\n**Recovery once the tag already exists** (this is safe and was done on 0.20.25):\n\n1. Confirm nothing was actually published — a failed `publish` dies *before* creating the\n   release, so there is usually nothing to undo:\n   ```bash\n   gh release view v<version> --repo RunanywhereAI/runanywhere-sdks   # expect \"release not found\"\n   ```\n2. Download the **exact artifacts the release will publish** (the candidate run named in\n   `publish_from_run_id`), verify each against its sidecar, and sync on a branch off `main`.\n3. Merge that branch, then **re-point the tag** at the new commit. `git push origin --delete`\n   can hang on flaky networks; the API is more reliable:\n   ```bash\n   gh api -X DELETE repos/RunanywhereAI/runanywhere-sdks/git/refs/tags/v<version>\n   gh api -X POST   repos/RunanywhereAI/runanywhere-sdks/git/refs \\\n       -f ref=\"refs/tags/v<version>\" -f sha=\"<new-main-sha>\"\n   ```\n   Only do this while **no release exists and nothing has been published**. If a release\n   was already published, do NOT move the tag — cut the next patch version instead.\n4. **Creating a tag via the API DOES trigger `release.yml`'s `push` trigger** (unlike a\n   `GITHUB_TOKEN` git-push of a tag, which does not). So immediately after re-tagging you\n   will have TWO runs: a full rebuild from the `push` event, and your own\n   `workflow_dispatch`. Identify by event and cancel the `push` one:\n   ```bash\n   gh run list --repo RunanywhereAI/runanywhere-sdks --workflow=release.yml --limit 3 \\\n       --json databaseId,event,headBranch --jq '.[] | \"\\(.databaseId) \\(.event) \\(.headBranch)\"'\n   gh api -X POST repos/RunanywhereAI/runanywhere-sdks/actions/runs/<push-run-id>/cancel\n   ```\n\nCheapest prevention: run `sync-checksums.sh --check <zip_dir>` against the candidate's\nartifacts and require **`N processed, 0 missing, 0 failed`** before you merge the release\nPR at all.\n\n**Recurred on v0.20.30 and again on v0.20.31** — same `mismatch: RACommonsBinary` /\n`mismatch: RACommons` failure, same root cause (step 2's candidate dispatch skipped\nentirely both times), fixed the same way both times: compute the real checksum from the\nalready-built release-artifact zip (`gh run download <failed-run-id> --name\nnative-ios-macos`), commit it on a fix branch, merge, then move the tag (§10d) to\nre-trigger `release.yml`. On v0.20.31 this ALSO surfaced the `RunAnywhereMLXRuntime`\npodspec entry as independently stale — `RAC_CHECKSUM_SKIP` means `publish` never checks\nit, so it would have shipped broken to Flutter consumers silently. **When fixing this\nfailure after the fact, check every row of the checksum-location table in step 3, not\nonly the archive named in the CI error** — the ones `RAC_CHECKSUM_SKIP` exempts are\nexactly the ones that won't tell you.\n\n---\n\n## 4. Watch for the \"package intentionally doesn't bundle a sibling dep\" bug class\n\nThis exact release hit a real bug in `bindings/web/scripts/package-sdk.sh`:\n`@runanywhere/web-onnx` deliberately does **not** bundle its own `proto-ts` copy (only\n`core`/`llamacpp` do — enforced by `bindings/web/scripts/validate_public_packages.py` and\nmirrored in `bindings/react-native/scripts/validate_public_packages.py`), but the\npackaging script's install-smoke-test (`verify_candidate_install`, around line 165 of\n`package-sdk.sh`) never supplied a resolution target for that unbundled dependency, so\n`npm install` reached out to the real npm registry for a not-yet-published version and\nfailed with `ETARGET`.\n\nFix pattern (already applied, keep it this way): the `onnx` call to\n`verify_candidate_install` passes the just-built local `proto-ts` tarball\n(`$PROTO_ARCHIVE`, built via `npm pack ../proto-ts`) as an explicit extra install arg —\nsee lines ~209-233 of `bindings/web/scripts/package-sdk.sh`, where `core` and `llamacpp`\ncalls omit it (they bundle proto-ts themselves) but `onnx`'s call appends `\"$PROTO_ARCHIVE\"`.\n\n**Generalize this as a standing check for every release**: grep for the asymmetric\nbundling flags before trusting any packaging script —\n\n```bash\ngrep -n \"expects_bundled_proto\\|--bundle\" \\\n    bindings/web/scripts/package-sdk.sh bindings/react-native/scripts/package-sdk.sh\n```\n\nWhenever a package intentionally does NOT bundle a sibling first-party dependency, its\nlocal pre-publish verification step must supply that dependency some other way (a local\ntarball) — the real registry version genuinely does not exist yet at candidate-build\ntime, and letting `npm`/`yarn` hit the real registry will `ETARGET`.\n\n**Before trusting any packaging script is bug-free, actually run it** against the real\ndownloaded native artifacts and check the exit code — do not just read the code and\nassume it's fine:\n\n```bash\ncd bindings/web && ./scripts/package-sdk.sh   # then check $?\ncd bindings/react-native && ./scripts/package-sdk.sh\n```\n\n### 4b. The two Electron-specific release bugs (both fixed — know the shapes)\n\nThe 0.20.25 train was the **first** release to run Electron packaging inside `release.yml`,\nand it surfaced two defects that no local test or green PR check could have caught. Both\nare fixed; recognize the shapes because they generalize.\n\n**(1) `sync-versions.sh` has a hardcoded package list, and new packages get missed.**\n`bindings/electron/packages/neurt/package.json` sat at the previous version while every\nsibling moved, because `neurt` was added (electron#734) after that list was last touched.\nSymptom: `native_win_x64_and_package` fails with\n\n```\nERROR: npm pack did not produce .../runanywhere-electron-neurt-<version>.tgz\n```\n\n— because `npm pack` correctly produced `...-neurt-<OLD-version>.tgz` from the stale\nmanifest. **Whenever you add a package anywhere, add it to `sync-versions.sh` the same\nday.** Cheap pre-flight check that catches the whole class:\n\n```bash\ngrep -h '\"version\"' bindings/electron/package.json bindings/electron/packages/*/package.json \\\n    bindings/electron/native/package.json | sort -u     # expect exactly ONE distinct version\n```\n\n**(2) `publish` downloads EVERY artifact, including intermediate hand-off bundles.**\n`publish`'s `Download all artifacts` step has no name filter. `electron-packaged-tarballs`\nis `native_win_x64_and_package`'s raw output whose only purpose is feeding `sdk_electron`,\nwhich re-uploads the curated public set as `sdk-electron`. Flattening both duplicated all\n5 Electron tarballs *and* dragged in the raw bundle's own `proto-ts` copy, which collided\nwith `sdk_web`'s:\n\n```\n::error::flatten collision: release-flat/runanywhere-proto-ts-<v>.tgz already exists\n::error::Refusing to publish — 6 filename collision(s) detected during flatten\n```\n\nFixed by pruning that directory in the flatten `find`\n(`-type d -name \"electron-packaged-tarballs\" -prune -o -type f \\( ... \\)`). **Standing\nrule: any new intermediate artifact must be either excluded from the flatten or named so\nit cannot collide.** When editing that `find`, test the exact expression against a fake\n`release-artifacts/` tree locally first — `-prune -o` + `-print0` is easy to get subtly\nwrong and the only other way to find out costs a full release run.\n\n---\n\n## 5. Resolve CodeRabbit (or any bot) review findings properly\n\nDo not resolve a review thread without a real, evidence-backed reply. Steps:\n\n1. Fetch unresolved threads via GraphQL:\n\n```bash\ngh api graphql -f query='\nquery($owner:String!, $repo:String!, $pr:Int!) {\n  repository(owner:$owner, name:$repo) {\n    pullRequest(number:$pr) {\n      reviewThreads(first:100) {\n        nodes { id isResolved comments(first:10) { nodes { id databaseId body } } }\n      }\n    }\n  }\n}' -f owner=RunanywhereAI -f repo=runanywhere-sdks -F pr=<pr-number>\n```\n\n2. For each unresolved thread, reply via REST with `in_reply_to` set to the comment's\n   `databaseId` (not the GraphQL node id), citing concrete evidence — a real run id,\n   a real checksum, a real commit SHA, not \"looks fine\":\n\n```bash\ngh api -X POST repos/RunanywhereAI/runanywhere-sdks/pulls/<pr-number>/comments \\\n    -f body=\"Verified against candidate run <run-id>: RunAnywhereMLXRuntime checksum\nmatches release-artifacts/native-ios-macos/RunAnywhereMLXRuntime-ios-v<version>.zip.sha256.\nSee commit <sha>.\" \\\n    -F in_reply_to=<comment-database-id>\n```\n\n3. Only then resolve, via GraphQL mutation on the thread id from step 1:\n\n```bash\ngh api graphql -f query='\nmutation($threadId:ID!) { resolveReviewThread(input:{threadId:$threadId}) { thread { isResolved } } }\n' -f threadId=<thread-node-id>\n```\n\n---\n\n## 6. Merge\n\nBefore merging, verify:\n\n```bash\ngh pr view <pr-number> --repo RunanywhereAI/runanywhere-sdks --json mergeable,reviewDecision\n```\n\nZero unresolved review threads (re-check the GraphQL query from step 5 — it should return\nnone with `isResolved: false`), and `mergeable == \"MERGEABLE\"`.\n\nConfirm bypass eligibility before relying on `--admin`:\n\n```bash\ngh api repos/RunanywhereAI/runanywhere-sdks/branches/main/protection \\\n    --jq '.required_pull_request_reviews.bypass_pull_request_allowances'\ngh api user --jq .login\n```\n\nIf your login appears in that allowance list (users, or a team/app you belong to):\n\n```bash\ngh pr merge <pr-number> --repo RunanywhereAI/runanywhere-sdks --squash --admin\n```\n\nIf not on the list, **stop** — ask a human to approve or merge instead of forcing it.\n`--admin` is a legitimate configured bypass only for allow-listed identities; do not use\nit to route around review for anyone else.\n\n---\n\n## 7. GOTCHA — cancel the auto-tag-dispatched rebuild, dispatch a publish-only run instead\n\nThe moment the PR merges, `auto-tag.yml` runs, pushes the `v<version>` tag, and **itself**\ndispatches a brand-new full `release.yml` run at that tag (see orientation section — plain\ntag pushes via `GITHUB_TOKEN` don't auto-trigger, so the workflow does it explicitly).\nThis is redundant — you already have a fully green, checksum-verified candidate from step\n2-3 — and wasteful, because Apple/MLX bytes will drift again for zero benefit (see the\nnon-reproducibility fact in step 3).\n\n**Cancel it immediately, before it burns real compute** (ideally within the first minute\nor two, before native_ios starts real work):\n\n```bash\ngh run list --repo RunanywhereAI/runanywhere-sdks --workflow=release.yml --limit 5\ngh api -X POST repos/RunanywhereAI/runanywhere-sdks/actions/runs/<auto-triggered-run-id>/cancel\n```\n\nThen dispatch a **publish-only** run that reuses your already-verified candidate's exact\nbytes instead of rebuilding:\n\n```bash\ngh workflow run release.yml --ref v<version> \\\n    -f version=<version> \\\n    -f publish_from_run_id=<candidate-run-id>\n```\n\n**CRITICAL SUB-GOTCHA**: if your candidate run in step 2 itself passed\n`reuse_native_web_run_id=<X>` to skip rebuilding WASM (a packaging-only re-release), that\ncandidate run **never uploaded its own copy of the `native-web` artifact** — it was\nskipped. `publish_from_run_id` alone will then fail during the release-manifest assertion\nwith `\"Missing or empty Web WASM payload\"`, because the publish job's artifact-download\nstep only pulls `reuse_native_web_run_id`'s artifact when that input is passed to *this*\ndispatch too (see `.github/workflows/release.yml` around the \"Download reused Web natives\"\nstep — it is gated on `inputs.reuse_native_web_run_id != ''`, independent of\n`publish_from_run_id`). You must **also** pass the same original run id:\n\n```bash\ngh workflow run release.yml --ref v<version> \\\n    -f version=<version> \\\n    -f publish_from_run_id=<candidate-run-id> \\\n    -f reuse_native_web_run_id=<X>\n```\n\nThis chains forward across releases: whichever run **originally** built `native_web` is\nthe run you keep citing at every subsequent `reuse_native_web_run_id`, not the most recent\nrun that merely borrowed it from an earlier one. Track that original run id explicitly if\nyou're doing back-to-back packaging-only releases.\n\n---\n\n## 8. Verify the draft release's asset set\n\nThe publish job only creates a **draft** GitHub Release (`softprops/action-gh-release`,\n`draft: true`) with built assets attached — it does not touch npm/Maven/pub.dev at all\n(grep `release.yml` for `npm publish`, `mavenCentral`, `pub publish` — none exist there).\n\nExpected asset count for a full, non-Windows-focused release: **~65 assets** —\n\n- 9 Apple archives (iOS/macOS XCFrameworks: RACommons + 5 backends + MLX runtime/Metal/\n  resources + CoreML/NeuRT)\n- 3 Android RACommons zips (arm64-v8a, armeabi-v7a, x86_64)\n- 1 Linux RACommons tar.gz\n- 1 Windows RACommons zip (preserved as an asset, advisory — see step 9)\n- 1 Web WASM tar.gz (`RACommons-web-v<version>.tar.gz`)\n- 3 rcli tarballs (macOS arm64, Linux x86_64, Windows x86_64 — macOS may also have a\n  notarized `.dmg` if Developer ID/notary secrets are configured; this is soft-skipped\n  otherwise, not a failure)\n- 1 Kotlin Maven repo zip\n- 8 npm tarballs (proto-ts, web core, web-llamacpp, web-onnx, RN core, RN llamacpp, RN mlx,\n  RN onnx)\n- 5 Electron npm tarballs (`runanywhere-electron-<v>.tgz` core, `-llamacpp`, `-onnx`,\n  `-sherpa`, `-neurt`) — added by `sdk_electron`; each has its own `.sha256` sidecar\n  counted separately below. **`electron-qhexrt` is deliberately excluded** — it still\n  gets built and packaged, but only as the `sdk-electron-qhexrt-private` workflow\n  artifact (not a release asset); download it separately for a manual `npm publish`.\n- 1 `MANIFEST.txt`\n- **explicitly ZERO** `qhexrt`-named files (the manifest assertion step hard-fails the run\n  if it finds any — confirm the run actually reached the publish job and didn't die there)\n\nEvery asset above except `MANIFEST.txt` has a `.sha256` sidecar, which is itself a\nseparate asset. **v0.20.25 landed at exactly 65 assets** — use that as the concrete\nbaseline rather than re-deriving the arithmetic.\n\nCross-check every checksum's `.sha256` sidecar against what got committed to\n`Package.swift` / the flutter `Package.swift` / the MLX podspec in step 3 — they must\nmatch byte for byte, because this run's artifacts are exactly what step 10 (sdk-publish)\nships to real consumers.\n\n```bash\ngh run view <publish-run-id> --repo RunanywhereAI/runanywhere-sdks --json jobs \\\n    --jq '.jobs[] | {name, conclusion}'\n\n# Asset count + the Electron set + the qhexrt-leak assertion, in three commands:\ngh release view v<version> --repo RunanywhereAI/runanywhere-sdks --json assets --jq '.assets|length'\ngh release view v<version> --repo RunanywhereAI/runanywhere-sdks --json assets \\\n    --jq '[.assets[].name | select(startswith(\"runanywhere-electron\"))] | sort'\ngh release view v<version> --repo RunanywhereAI/runanywhere-sdks --json assets \\\n    --jq '[.assets[].name | select(test(\"qhexrt\";\"i\"))]'          # MUST be []\n```\n\n### Prove the artifacts are real — don't trust green checkmarks\n\nA green `publish` only proves the assertions ran. To prove the bytes are right, run the\nrepo's own fail-closed gate against the **published** tarballs. This is the single highest\n-value verification in the whole runbook and it takes about a minute:\n\n```bash\ngh release download v<version> --repo RunanywhereAI/runanywhere-sdks \\\n    -p 'runanywhere-electron*.tgz' -D /tmp/verify\npython3 scripts/validation/gates/check_plugin_natives.py --expect-version <version> \\\n    $(for t in /tmp/verify/*.tgz; do printf -- '--tarball %s ' \"$t\"; done)\n```\n\nExpect `All N check(s) passed: plugin natives are routable and no staging-URL placeholder\nwas found.` On v0.20.25 that was **20 checks** across 5 tarballs, asserting three separate\nthings per binary: the real `STAGING_BASE_URL` is baked in (no `YOUR_STAGING_BASE_URL`\nplaceholder), `rac_commons` embeds the right version string, and each backend is actually\n**routable** (real op tables — `g_llamacpp_ops`, `g_sherpa_stt_ops`, `g_neurt_llm_ops`,\netc.), which is what distinguishes a real plugin from a same-sized shell.\n\nSanity-check the shipped layout too — a tarball missing a platform is a silent regression:\n\n| Package | Expected `prebuilds/` |\n|---|---|\n| `@runanywhere/electron` | `darwin-arm64` + `win32-x64` + `win32-arm64` |\n| `-llamacpp`, `-onnx`, `-sherpa` | `darwin-arm64` + `win32-x64` |\n| `-neurt` | `darwin-arm64` only (Apple Neural Engine) |\n| `-qhexrt` (private artifact) | `win32-arm64` only (Hexagon NPU) |\n\n---\n\n## 9. Windows handling — build/preserve only, never auto-publish\n\n`native_windows` and `native_rcli_windows` run and their outputs are **preserved** as\nrelease assets (proving the binaries build), but nothing Windows-specific is published to\nany package registry as part of the standard flow unless a human explicitly asks for it —\nthat's a deliberate, separate decision, not an oversight.\n\nFor anything that needs actual execution on Windows (smoke-testing the native Windows\nbuild, or Electron's Windows packaging), do not assume a macOS cross-build proves\nWindows correctness — SSH to the real Windows box. The team keeps a **Windows ARM64\nmachine reachable over SSH**; its hostname, user, key, and mesh-VPN node name are\noperator-specific and deliberately not recorded in this repo — read the Windows/ARM64\nhost entry out of the operator's `~/.ssh/config` and use that alias. If there is no such\nentry, the box is not configured for you: say so and ask, rather than guessing.\n\n```bash\nWIN_BOX=<the ssh alias from ~/.ssh/config>\n# Ask the mesh-VPN client whether the node is up FIRST (e.g. `tailscale status`,\n# matching on that host entry's HostName) — never just try SSH and retry on timeout.\nssh -o ConnectTimeout=8 \"$WIN_BOX\" \"whoami\"\n```\n\nIf the VPN reports the node offline/asleep, **that's not a config problem** — ask the\nuser to wake the box; don't debug SSH config against a machine that's simply off.\n\n---\n\n## 10. Publish the draft, then hand off\n\nOnce the draft's asset set is verified complete and correct (step 8):\n\n```bash\ngh release edit v<version> --repo RunanywhereAI/runanywhere-sdks --draft=false --latest\n```\n\nThis is the point where SwiftPM's `from: \"<version>\"` resolution and the download URLs\nbaked into `Package.swift` start actually working for real external consumers — draft\nrelease assets are not publicly downloadable, so nothing before this line is\nexternally live.\n\n**Hand off to the `sdk-publish` skill** for the actual npm / Maven Central / pub.dev /\nSwiftPM-distribution-repo publishing steps. This skill's job ends here.\n\n---\n\n## 10b. MANDATORY, and it turns `main` RED until you do it — cut runanywhere-swift\n\nThe moment `v<version>` exists in this repo, `check_swift_dist_repo_sync.sh` (run by\n`pr-build.yml`'s `centralization` job) **fails on every PR and every push to `main`**:\n\n```\n[FAIL] runanywhere-swift has no 0.20.25 tag (latest: 0.20.24)\n       v0.20.25 is published here, so 'from: \"0.20.25\"' is broken for every\n       SwiftPM consumer of https://github.com/RunanywhereAI/runanywhere-swift.git\n```\n\nThis is **by design**, not a bug, and it is the #1 reason a release looks \"not green\"\nafter everything else succeeded. Do not go hunting for a code defect — cut the repo:\n\n```bash\ngit clone https://github.com/RunanywhereAI/runanywhere-swift.git /tmp/ra-swift\nbindings/swift/scripts/sync-dist-repo.sh --zips <zip_dir> --tag /tmp/ra-swift\n# verify BEFORE pushing (content check passes locally; the tag check needs the push)\nRUNANYWHERE_SWIFT_DIST_REPO=/tmp/ra-swift \\\n    bash scripts/validation/gates/check_swift_dist_repo_sync.sh\ngit -C /tmp/ra-swift push origin main --follow-tags\nbash scripts/validation/gates/check_swift_dist_repo_sync.sh    # now [OK]\n```\n\nGotchas found doing this on 0.20.25:\n\n- `--check` **cannot be combined** with `--zips`/`--commit`/`--tag` (hard error). Run a\n  bare `--check <repo>` first to preview, then the real invocation separately.\n- `--check` legitimately reports `ERROR: distribution repo is out of sync` before you\n  sync — that's the preview telling you what it will change (expect the `sdkVersion` bump,\n  ~3 changed files, `0 added / 0 removed`). Non-zero exit there is not a failure.\n- The script **never pushes**; `--tag` only creates the tag locally. The dist repo tag has\n  **no `v` prefix** (`0.20.25`, not `v0.20.25`) while the monorepo tag does.\n- Running the gate locally right after `--tag` still shows `[FAIL] ... has no <version>\n  tag` while simultaneously showing `[OK] ... checkout matches: all 6 checksums`. That is\n  expected: the content check reads your local checkout, the tag check queries the real\n  remote. It clears the moment you push.\n- It chains into `sync-checksums.sh`, so run it **after** step 3's checksums are committed,\n  or the dist manifest gets stale hashes.\n\n---\n\n## 10c. Windows + self-hosted-runner traps (read before debugging a red Windows job)\n\nThese all cost a full CI cycle. The unifying fact:\n\n> The self-hosted runner service executes as `NT AUTHORITY\\NETWORK SERVICE`, which has its\n> own PATH **and** its own NTFS permissions. Anything you verify over SSH on that box tells\n> you nothing about what the job can do. `native_win_arm64_qhexrt` already carries a long\n> comment about this — read it before inventing a fix, and keep it in sync with neurun's\n> equivalent lane, which runs on the *same machine*.\n\n**Environment**\n\n- **`bash: command not found`** — Git for Windows puts `Git\\cmd` (git.exe) on PATH but not\n  `Git\\bin` (bash.exe). `actions/checkout` still works because it calls git.exe directly,\n  which makes this look unrelated to runner setup.\n- **`pwsh` does not exist on that box** — Windows PowerShell 5.1 only. Use\n  `shell: powershell`.\n- **`gh` is not installed on that box, and must not be assumed anywhere.** A QHexRT\n  download died on `gh: command not found`. The fix is deliberately *not* to install it:\n  every fetch of a private release asset now goes through\n  `fetch_release_asset()` in `scripts/build/_release_asset.sh`, which uses `curl` + the\n  REST API. `curl` ships with Windows, macOS and every Linux image we use, so this is one\n  code path everywhere instead of a per-runner provisioning step. If you add a new\n  consumer of a private asset, source that helper — do not reach for `gh`.\n  Note the helper matches the asset name **exactly**: a prefix match picks the `.sha256`\n  sidecar (its name starts with the tarball's), and you get a 65-byte \"archive\" that fails\n  much later with a confusing error.\n- **Tool availability is the single most common failure class on this runner** — `bash`,\n  `python3`, `ninja`, `gh`, GNU `tar` and NDK `.exe` suffixes have each broken a lane\n  once. When a self-hosted job fails, check \"does this tool exist as spelled\" before\n  reading any logic.\n- **`ln -s` under Git-Bash COPIES the directory** unless `MSYS=winsymlinks:nativestrict`.\n  It does not fail, so the result looks like real content and whatever validates a\n  \"must be a symlink\" invariant rejects a payload that was actually fine. Never use shell\n  `ln -s` on this box — go through `scripts/build/_selection.py`.\n- **Windows selects with a JUNCTION, not a symlink, and that is deliberate.** The runner\n  service account cannot create symlinks (below), but a junction is the same kind of\n  reparse point to a directory and needs no privilege. Two traps when reading that code:\n  `Path.is_symlink()` returns **False** for a junction (`stat.S_ISLNK` is true only for\n  `IO_REPARSE_TAG_SYMLINK`) — use `_selection.is_selection()`; and a junction stores an\n  **absolute** t","AGENTS.md":"# AGENTS.md\n\nGuidance for AI coding assistants working in this repository.\n\n> **`AGENTS.md` is the real file; each `CLAUDE.md` is a committed symlink to the\n> `AGENTS.md` beside it.** Editing either name edits the same bytes. If you create a new\n> `AGENTS.md` in a directory that doesn't have one, run\n> `bash scripts/validation/gates/check_agents_claude_sync.sh --fix` from the repo root to\n> create its `CLAUDE.md` symlink — `pr-build.yml` fails any tracked `AGENTS.md` missing one.\n> Never hand-create a `CLAUDE.md` as a real file.\n\n## Working conventions\n\n- **Focus on simplicity.** Clean, SOLID-ish separation of concerns; reuse over\n  reinvention. No mock implementations and no new unit tests unless the task explicitly\n  asks for them.\n- **Plan first for non-trivial work.** Write the plan to\n  `thoughts/shared/plans/{descriptive_name}.md`, get it approved before implementing, and\n  keep it updated (append what changed) as you go so the next engineer can pick it up.\n- **Structured types over raw strings.** Enums/sealed classes/data classes for\n  configuration and results — this is enforced per-SDK by lint (ktlint/detekt, SwiftLint,\n  ESLint) so ad hoc strings tend to get caught anyway.\n- **Read files fully** before editing them; only use offset/limit on files too large to\n  read at once.\n- **Use full local build parallelism** (`-j \"$(sysctl -n hw.logicalcpu)\"` / equivalent),\n  scaling down only under real memory/thermal pressure — not because load average looks\n  high.\n- **Multi-agent workflows run on Sonnet.** When fanning work out to subagents (the\n  `Workflow` tool, or several parallel `Agent` calls), pass `model: 'sonnet'` on every\n  `agent(...)` call rather than letting them inherit the session model. A fan-out of\n  8-10 probes reading large files is exactly the shape where Sonnet is the right\n  cost/latency trade, and these agents are doing bounded, well-specified retrieval and\n  analysis rather than open-ended reasoning. Reserve the session's stronger model for\n  the synthesis step that consumes their reports, and for anything where a wrong answer\n  is expensive and hard to detect — a final design decision, an adversarial verification\n  pass, or a security-relevant judgement.\n\n  ```js\n  // in a Workflow script\n  const findings = await parallel(LENSES.map((l) => () =>\n    agent(l.prompt, { label: `probe:${l.key}`, schema: FINDINGS, model: 'sonnet' })))\n\n  // the synthesis that reads them can stay on the session model\n  const plan = await agent(synthesisPrompt, { schema: PLAN, effort: 'high' })\n  ```\n\n## The most important architectural rule: logic lives at the lowest layer that serves everyone\n\nEach feature/modality (LLM, STT, TTS, VAD, VLM, RAG, LoRA, Voice) is invoked through\n**one** SDK entry point; the SDK, and below it C++ commons, does all the heavy lifting. If\nan example app builds a multi-step bootstrap sequence, hardcodes a model/engine constant,\nor post-processes model output, that is a bug in the SDK, not the app — fix it down a\nlayer.\n\n1. **C++ commons (`core/`)** — cross-platform, non-I/O logic: model lifecycle, registry,\n   download orchestration, RAG session management, inference routing. All 5 SDKs get the\n   fix for free.\n2. **Platform SDK layer** — platform-specific I/O or runtime bridging only (Web OPFS,\n   iOS Keychain, Android Keystore, WASM MEMFS mirroring).\n3. **Example apps** — UI rendering, navigation, thin SDK calls. No business logic, no\n   internal-SDK knowledge (path patterns, framework→directory mappings), no workarounds.\n\n**iOS is the source of truth.** When behavior is ambiguous in any other SDK, check the\niOS Swift implementation first and copy the logic exactly, adapting only syntax.\n\n---\n\n## Repository overview\n\nCross-platform on-device AI SDK monorepo. A single C/C++ core (`runanywhere-commons`,\n~118K first-party LOC plus ~420K generated proto bindings) implements all AI business\nlogic behind a pure C ABI (`rac_*` prefix). Platform SDKs are thin bridges that supply\nplatform services (file I/O, HTTP, Keychain, audio) via an inversion-of-control struct and\ncall into the C core for all inference. Protobuf IDL schemas generate type-safe bindings\nfor every language.\n\n**Current version**: `0.20.37` (canonical source: `core/VERSION`)\n\n| SDK | Path | Bridge mechanism | Platforms |\n|-----|------|-------------------|-----------|\n| Swift | `bindings/swift/` | XCFramework + CRACommons module map | iOS 17.5+, macOS 14.5+ |\n| Kotlin | `bindings/kotlin/` | JNI (`librunanywhere_jni.so`) | Android (min API 24) |\n| Flutter | `bindings/flutter/` | Dart FFI | iOS, Android |\n| React Native | `bindings/react-native/` | NitroModules (JSI HybridObject) | iOS 17.5+, Android arm64 |\n| Web | `bindings/web/` | Emscripten WASM + TypeScript | Chrome, Safari, Firefox |\n| Electron | `bindings/electron/` | Node-API addon | Windows x64 (preview) |\n| Python | `bindings/python/` | pybind11 extension (`runanywhere._core`) | Windows, macOS, Linux (alpha) |\n\nEach has its own `AGENTS.md` with full architecture, build commands, and conventions —\nread it before working in that SDK.\n\n| Native dir | Contents |\n|-----------|----------|\n| `core/` | C/C++ core library: all AI logic, plugin registry, event system |\n| `engines/` | 7 backend plugins: llamacpp, sherpa, onnx, cloud, mlx, qhexrt, neurt |\n| `runtimes/` | 3 runtime adapters: cpu (always), onnxrt, coreml |\n| `idl/` | 39 Protobuf schemas + per-language codegen scripts |\n\n### Consumer apps live in separate repos\n\nThe four full consumer apps were extracted into standalone repos (history preserved); PR\nagainst them there, not here: [runanywhere-ios](https://github.com/RunanywhereAI/runanywhere-ios),\n[runanywhere-android](https://github.com/RunanywhereAI/runanywhere-android),\n[runanywhere-web](https://github.com/RunanywhereAI/runanywhere-web),\n[runanywhere-electron](https://github.com/RunanywhereAI/runanywhere-electron).\n\nTwo example apps remain in-tree (`bindings/flutter/example/`, `bindings/react-native/example/`).\nAll example apps share brand orange `#FF6900`, documented in `docs/DESIGN_GUIDELINE.md`.\n\n### Minimal examples are how you verify an SDK change locally\n\nEach in-repo example consumes its SDK from local source (no staging/publishing needed to\nsee an edit): Swift via `RUNANYWHERE_USE_LOCAL_NATIVES=1`, Kotlin via a Gradle composite\nbuild (`includeBuild` + `dependencySubstitution`), Web via Vite aliases into\n`packages/*/src`. They're deliberately minimal (one prompt in, one streamed completion\nout) — feature-complete UI belongs in the consumer repos above.\n\n---\n\n## Cross-platform architecture\n\n`idl/*.proto` is the schema root; `idl/codegen/generate_all.sh` emits per-language\nbindings (Swift, Kotlin/Wire, TS/ts-proto, Dart), all generated and none committed — see\n[`docs/reference/generated-code-contract.md`](docs/reference/generated-code-contract.md)\nfor the full \"what generates what, when\" contract before touching `idl/`.\n\nAll SDKs reach `runanywhere-commons` through the `rac_*` C API. Commons holds the\ncomponent layer (lifecycle), the service layer (dispatch), and the plugin registry, and\nreaches engines through `rac_engine_vtable_t`.\n\n| Engine | Primitives |\n|---|---|\n| llamacpp | LLM, VLM |\n| sherpa-onnx | STT, TTS, VAD |\n| onnx | Embed, Segment |\n| qhexrt | Hexagon NPU |\n| neurt, cloud | Apple Neural Engine, HTTP |\n\nBackend base priorities (highest wins per primitive via `rac_plugin_find()`, no\nruntime/format scoring): qhexrt=150 (QNN-context models only), mlx=110 (Apple),\nllamacpp=100, sherpa=90, onnx/cloud=50. An explicit engine name is honored through\n`rac_plugin_find_for_engine()` regardless of priority.\n\n### Non-obvious invariants that span every SDK\n\n- **Platform adapter IoC**: `rac_platform_adapter_t` is a flat C struct of function\n  pointers populated by each SDK before `rac_init()`. C++ never calls platform APIs\n  directly — file I/O, HTTP, Keychain, logging, memory queries all pass through it.\n- **Two-phase init**: every SDK does Phase 1 (sync: register adapter, load native libs,\n  configure logging) then Phase 2 (async: authenticate, register device, fetch model\n  assignments, discover downloaded models).\n- **Plugin ABI**: `RAC_PLUGIN_API_VERSION = 9u`; a version mismatch is an immediate\n  rejection. Each vtable has 10 active primitive slots and 7 reserved. NULL slot = not\n  supported. `rerank_ops` (wire value 11) was promoted from `reserved_slot_2` in ABI v8;\n  wire value 6, retired in ABI v4, stays permanently retired — never reuse it. See\n  `core/AGENTS.md` for the full ABI history and how to add a primitive.\n- **Static vs. dynamic plugins**: iOS and WASM force `RAC_STATIC_PLUGINS=ON` (no\n  `dlopen`), registered via `RAC_STATIC_PLUGIN_REGISTER(name)` + `-force_load` /\n  `--whole-archive`. Android/Linux/macOS default to dynamic loading via\n  `rac_registry_load_plugin()`.\n- **Streaming fan-out**: C++ allows only one proto-byte callback per component handle.\n  Each SDK implements its own fan-out to multiplex that into multiple subscribers (Swift\n  `AsyncStream`, Kotlin `Flow`, Dart `StreamController`, TS `AsyncIterable`).\n- **Proto types are canonical**: never hand-write enum values or structured types that\n  exist in `idl/*.proto` — use the generated types/typealiases and regenerate instead.\n- **HTTP is platform-provided**: no libcurl. Each SDK registers a\n  `rac_http_transport_ops_t` (URLSession on Apple platforms, OkHttp on Android,\n  `emscripten_fetch` on Web).\n- **NeuRT crosses a repo boundary as BYTES, not source**: the Apple engine is built and\n  published by the private `neurun` repo and consumed here as prebuilt archives, pinned by\n  tag + SHA-256 in `core/VERSIONS` (`NEURT_*`). Fetch with `scripts/build/download-neurt.sh`.\n  Each archive carries a receipt naming the `RAC_PLUGIN_API_VERSION` it was built against,\n  and both the downloader and CMake refuse a mismatch — a vtable-layout change relinks\n  cleanly and corrupts dispatch at runtime, so nothing else would catch it.\n\nA side-by-side comparison of entry point / bridge / streaming / events / storage / HTTP\nper SDK — useful when porting a fix across SDKs — lives in\n[`docs/reference/cross-sdk-parity.md`](docs/reference/cross-sdk-parity.md).\n\n---\n\n## Building and running\n\nThe root `CMakeLists.txt` (version from `core/VERSION`) is the single entry point for\nnative builds; `CMakePresets.json` defines `macos-{debug,release}`, `linux-{debug,release,asan}`,\n`ios-{device,simulator}`, `android-arm64`, `wasm`, `cpp-desktop-*`, and the `windows-*` presets.\n\n```bash\ncmake --preset macos-debug && cmake --build build/macos-debug && ctest --preset macos-debug\n```\n\n**`./run <group> [subcommand]`** is the unified wrapper for everything else — build,\nlint, and run any SDK or example app from one CLI (`./run --help` for the full menu):\n\n```bash\n./run doctor                    # scan host toolchains, show what's buildable here\n./run setup                     # provision env for every host-buildable target\n./run sdk commons build-android # build C++ commons for all Android ABIs, stage .so files\n./run sdk kotlin build          # (etc. — see per-SDK AGENTS.md for direct tool commands)\n./run example android install   # build + install + launch the Kotlin minimal example\n./run codegen                   # regenerate every language binding from idl/\n```\n\nFor the direct `swift build` / `./gradlew` / `melos` / `yarn` / `npm` commands each SDK\nuses under the hood, and its test/lint/publish targets, see that SDK's own `AGENTS.md`\n(`bindings/<name>/AGENTS.md`) — the commands are kept there, not duplicated here, so they\ncan't drift.\n\nCross-platform build scripts worth knowing about at the root level:\n\n```bash\n./bindings/swift/scripts/build-core-xcframework.sh   # also syncs into RN/Flutter plugin dirs\n./scripts/build/build-core-android.sh                # .so for all ABIs → every SDK's jniLibs/\n./bindings/web/scripts/build-core-wasm.sh\n./scripts/setup/setup-toolchain.sh                   # protoc/wire/ts-proto toolchain\n./idl/codegen/generate_all.sh [--only <lang>]         # swift|kotlin|dart|ts|cpp|python\n```\n\n---\n\n## Version management and releases\n\nCanonical version: `core/VERSION` (single-line semver). Bump everywhere with:\n\n```bash\n./scripts/release/sync-versions.sh <version>\n```\n\nThe full release runbook (version bump → PR → tag → GitHub Release → npm/Maven/pub.dev/\nSwiftPM publishing → cutting the `runanywhere-swift` SPM distribution repo → starter-app\nsmoke tests) is a multi-step process captured in three skills — use them rather than\nimprovising the steps: **sdk-release** (version bump through published GitHub Release),\n**sdk-publish** (registry publishing + the `runanywhere-swift` dist repo cut), and\n**sdk-test-starters** (post-release device/app smoke tests).\n\n### Release pipeline lessons (v0.20.29 through v0.20.33 — read before cutting a release by hand)\n\n- **`native_ios` and `native_android` always rebuild fresh archives on every `release.yml`\n  run — there is no reuse/skip mechanism for either job** (unlike `native_web`'s\n  `reuse_native_web_run_id` or the cpp-desktop kits' `reuse_cpp_desktop_run_id`). This makes\n  the Apple/Android TEMP-pin convention (Package.swift's `sdkVersion`, the Flutter podspecs'\n  `asset_version`, Kotlin's `nativeLibVersion` pinned to an old version \"because this release\n  ships no new iOS/Android archives\") **structurally unsound for any release where those two\n  jobs actually run — which is every release.** It surfaced as three separate bugs\n  (`auto-tag.yml`'s coherence check, `package-sdk.sh`'s podspec check, and `sync-checksums.sh`'s\n  publish-time verification) before the pattern was recognized. If a release genuinely ships\n  no new Apple/Android bytes, that has to be proven by diffing the actual built artifacts\n  against the previous release, not assumed from what changed in the diff.\n- **`RunAnywhereMLXRuntime`'s stripped Mach-O is not byte-reproducible across builds**\n  (`RAC_CHECKSUM_SKIP=RunAnywhereMLXRuntime` in `release.yml`'s publish job exists because of\n  this, and is the ONLY archive exempted from the publish-time checksum gate). The corollary\n  that bit v0.20.29: **the checksum committed to `Package.swift` / the Flutter MLX podspec\n  can only be computed from a build that already happened, but the tag that freezes those\n  files has to be pushed before `native_ios` (which produces that exact artifact) even runs**\n  — so re-syncing checksums against an EARLIER, superseded build (e.g. after a tag had to be\n  moved and re-dispatched to fix an unrelated bug) commits a value that does not match what\n  the FINAL successful run actually publishes, and the skip means nothing catches it. v0.20.29\n  shipped this way (committed `d942788f...`, actually published `fc4b23a0...`) — caught only\n  because the Flutter `runanywhere_mlx` package hadn't been published to pub.dev yet for that\n  version. Before publishing any package that ships `RunAnywhereMLXRuntime`, re-run\n  `bindings/swift/scripts/sync-checksums.sh` against the artifacts from the SPECIFIC run whose\n  tag is being published — never reuse a checksum computed against an earlier attempt.\n- **The Electron QHexRT backend never set `ADSP_LIBRARY_PATH`, and one comment in the\n  codebase confidently asserted Windows doesn't need it.** It does, for exactly one decode\n  path: `QHexRT/src/bonsai/fastrpc_win.cpp`'s custom FastRPC skel (the ternary/Bonsai\n  decoder, e.g. `qwen3.8-27b-1bit-npu`) resolves through `ADSP_LIBRARY_PATH ∪ cwd`, separately\n  from the standard QNN HTP graph path's `LoadLibraryW` + PATH search that `bridge.ts`'s\n  `addSidecarDirToDllSearch` already handled. Every OTHER QHexRT model worked fine in the\n  packaged app, which is exactly what let this ship undetected — validate the Bonsai/ternary\n  path specifically, not just \"some NPU model works.\" Fixed in `bridge.ts`'s\n  `addSidecarDirToDspSearchPath` (#803).\n- **`package-private-engine-overlay.sh`'s QAIRT-runtime copy step filtered on `{\".dll\",\n  \".lib\"}` only**, silently dropping every `.so`/`.cat` skel file (both the standard HTP skel\n  and, since it never read `engines/qhexrt/prebuilt/current/dsp/win-arm64/` at all, the\n  Bonsai skel too). This meant **RCLI could not have run any QHexRT model** — standard or\n  ternary — from a fresh private overlay, a gap invisible until someone actually tried to run\n  inference rather than just building. `bindings/electron/scripts/bundle-native.ts` already\n  had the correct pattern (copy `dsp/win-arm64/` flat, no extension filter); the overlay\n  script just never matched it. Fixed in #804.\n- **A new NeuRT archive must be added in FOUR places, and three of them only fail in a build\n  configuration your Mac never produces.** v0.20.33 added `libneurt_rac_tts_ops.a` and took two\n  wasted release candidates to land, because each site fails somewhere different and late:\n  `engines/neurt/CMakeLists.txt` (probe list + imported target + `_neurt_extra_links`),\n  `build-core-xcframework.sh` (pre-flight guard AND `_NEURT_ROUTABLE_ARCHIVES` — two lists in one\n  file), `package-private-engine-overlay.sh`, and the `#if RAC_NEURT_ROUTABLE` guard around the\n  vtable entry in `rac_plugin_entry_neurt.cpp`. Miss the force-load lists and every native job goes\n  green while `validate_consumer_flutter` dies an hour later on `Undefined symbol:\n  _g_neurt_tts_ops` — the engine registers, all CMake targets compile, and only a CONSUMER's link\n  fails. Miss the vtable guard and the routable build is fine while every PR job WITHOUT\n  `NEURUN_TOKEN` fails, because a non-routable shell still references a symbol nothing defines.\n  A dev machine with the payload staged is routable, so it reproduces neither. `libneurt_rac_*.a`\n  archives now fail packaging by name if they are not in the list\n  (`neurt_assert_no_unlisted_archives`); the vtable guard has no such mechanism — audit all ten\n  slots when adding one. Reproduce a non-routable build by moving `core/third_party/neurt` aside.\n- **`reuse_native_web_run_id` is SAME-VERSION-ONLY, whatever the diff looks like.** `racommons.wasm`\n  embeds `RAC_VERSION_STRING` and `validate_public_packages.py` asserts it against the version being\n  packaged, so reusing ANY prior release's run fails `sdk_web` with *\"does not embed\n  RAC_VERSION_STRING\"*. Worse than a red job: `native_web` is skipped, so that candidate never\n  uploads a web artifact and can never serve as `publish_from_run_id`. Only a re-dispatch at the\n  same version qualifies.\n- **Verify checksums by NAME as well as by URL — the manifests use two different shapes.** The root\n  `Package.swift` interpolates `\\(sdkVersion)` into each URL; the Flutter manifests bind a checksum\n  to a `name:` through a helper with no URL at the call site at all. A URL-only cross-check silently\n  validates 9 of 15 sites and reports success. Re-hash every site against the zip it names, and\n  remember `RAC_CHECKSUM_SKIP` hides `RunAnywhereMLXRuntime` from `publish` entirely, so the Flutter\n  MLX podspec is verifiable only by hand.\n- **A green `rcli backends --json` proves nothing against the OSS kit.** NeuRT ships as a separate\n  private pack, so the public bottle lists no `neurt` at all — a routability check there passes\n  cleanly and means nothing. Apply the private overlay\n  (`cpp-desktop-macos-arm64-neurt-private` workflow artifact) before believing the result.\n- **General pattern across all four of the above**: each one was invisible to the checks that\n  existed (a version-coherence check, a checksum-skip escape hatch, a \"the model loaded\"\n  smoke test, a \"the build succeeded\" CI job) because none of those checks actually ran the\n  specific thing that was broken. When validating a release, run the SPECIFIC flagship/hardest\n  model end-to-end on-device, not just confirm the pipeline completed.\n- **The RACommons/RunAnywhereMLXRuntime checksum-staleness bug above recurred on v0.20.30 AND\n  v0.20.31** — same `mismatch: RACommonsBinary` failure in `publish`, same root cause both\n  times: the version-bump PR ran `sync-versions.sh` and merged/tagged directly, skipping the\n  **mandatory** \"dispatch a release candidate off the unmerged branch, sync checksums from\n  its real build, THEN merge\" sequence that the **`sdk-release` skill's step 2-3 already\n  document in detail** (including this exact failure signature). Documenting the lesson in\n  prose here was not enough to stop it recurring twice more — **if you are about to run\n  `scripts/release/sync-versions.sh` for ANY reason, including as one step of a larger\n  cross-repo release cascade, invoke the `sdk-release` skill and follow it from step 1**,\n  rather than reconstructing the sequence from memory or treating a \"small\" version bump as\n  exempt. v0.20.31 additionally caught `RunAnywhereMLXRuntime` in the Flutter podspec\n  independently stale in the SAME commit — `RAC_CHECKSUM_SKIP` exempts it from `publish`'s\n  check, so it would have shipped broken to Flutter consumers with no CI signal at all; check\n  every row of the checksum-location table, not only the one the CI error names.\n- **QHexRT's `qhx_runtime_device()` (arch/soc lookup, needed to pick the `v75`/`v79`/`v81`\n  manifest directory in `session_open()`) shared its live-device cost with the standard QNN\n  graph path**, so a `host_only` model with ZERO QNN graphs — the Bonsai/Maple ternary decoder\n  (`qwen3.8-27b-1bit-npu`) — paid for a live QNN HTP device anyway, which then contended with\n  the decoder's own direct FastRPC session for the same Hexagon cDSP and made every generation\n  fail with a bare `HostOpFailed`. This shipped in v0.20.30 (the SAME release that fixed the\n  `ADSP_LIBRARY_PATH` bug for the same model) and was found only by running that SPECIFIC model\n  on real Windows-ARM64 hardware after the \"fix\" — reinforcing the point above: a green\n  `HostOpFailed`-free run of a DIFFERENT QHexRT model proves nothing about this one. Two red\n  herrings were chased and ruled out with live device tracing before the real cause: neither\n  `ADSP_LIBRARY_PATH` (confirmed correctly set) nor a QAIRT-version mismatch between the\n  shipped host DLLs and the on-device skel (confirmed present, but fixing it alone did not fix\n  the failure) was the actual cause. Fixed in `neurun` v0.20.31 (`Backend::profile()` now uses\n  a separate, device-handle-free `deviceGetPlatformInfo` query instead of sharing\n  `ensure_device()` with `device()`) + this repo's `qhexrt_session.cpp` (#810, a complementary\n  fix removing a second, redundant eager device query used only for a log line).\n\n---\n\n## CI/CD workflows (`.github/workflows/`)\n\n| Workflow | Trigger | Purpose |\n|----------|---------|---------|\n| `pr-build.yml` | PR to main, push to main/feat branch | Parallel native builds (macOS/Linux/iOS/Android) + per-SDK typecheck + centralization/coherence gates |\n| `release.yml` | Tag `v*.*.*` or manual | Full artifact build matrix, SDK packaging, consumer validation, draft Release |\n| `auto-tag.yml` | PR merged to main with `release:*` label | Verifies the reviewed semver bump, pushes that exact git tag |\n| `idl-drift-check.yml` | Changes to `idl/` or generated files | Regenerates protos, verifies schema lock + untracked generated trees |\n| `legacy-files-blocklist.yml` | All PRs/pushes | Prevents specific deleted files from being re-introduced |\n| `secret-scan.yml` | PRs and pushes to main | Incremental gitleaks scan on diff range |\n| `check-no-pii-logging.yml` | PRs/pushes to main/master/feat-branch | Guards against logging signed URLs alongside active-download destination paths |\n\n---\n\n## Platform requirements\n\n| Platform | Min version | Build tool | Key versions |\n|----------|------------|------------|--------------|\n| iOS / macOS | 17.5 / 14.5 | Xcode 26+ | Swift 6.2 |\n| Kotlin SDK | Android API 24 | AGP 9.2.1 / Gradle 9.5.0 | Kotlin 2.4.0, NDK 27.3.13750724 |\n| Flutter | 3.44.6 | Melos / AGP 9.0.1 / Gradle 9.1.0 | Dart 3.12.2+, NDK 28.2.13676358 |\n| React Native | 0.85.3 (min 0.83.1) | Yarn Berry 3.6.1 | NitroModules, Hermes |\n| Web | Chrome 86+ | Vite | Emscripten 6.0.2, Node 24 LTS |\n| C++ core | N/A | CMake 3.24+ | C++20, Ninja |\n\n---\n\n## Non-obvious configuration details\n\n- **NDK pin**: `core/VERSIONS::NDK_VERSION` (27.3.13750724) is the single source of truth,\n  mirrored into `bindings/kotlin/gradle.properties`. NDK 27 (r27d) gives 16 KB\n  page-alignment required by Android 15+ — NDK 25.x's 4 KB-aligned `libc++_shared.so` /\n  `libomp.so` trips Android 16's page-size enforcement. Mirror this pin whenever bumping\n  it; Flutter/RN Android build files carry their own fallback literals that can drift.\n- **`useLocalNatives`** (Kotlin `gradle.properties`, similarly named flags elsewhere):\n  `true` builds native libs from source locally; CI and most non-local runs set it\n  `false` to download prebuilt `.so`/`.xcframework`/`.wasm` from GitHub Releases instead.\n- **Web SDK** has several hard-won WASM/browser runtime workarounds (VLM worker crash\n  recovery, a Qwen2-VL WebGPU NaN bug, cross-origin isolation requirements) — see\n  [`docs/reference/web-runtime-gotchas.md`](docs/reference/web-runtime-gotchas.md).\n- **Cloud/Linux dev VM** (e.g. Cursor Cloud): what's buildable there and the\n  clang/NDK/KVM gotchas are in\n  [`docs/reference/cursor-cloud-environment.md`](docs/reference/cursor-cloud-environment.md).\n\n---\n\n## Pre-commit hooks\n\n```bash\npre-commit run --all-files                     # everything\npre-commit run ios-sdk-swiftlint --all-files   # SwiftLint only\n```\n\nConfigured hooks: gitleaks (secrets), trailing-whitespace, end-of-file-fixer, check-yaml,\ncheck-added-large-files (1000 KB max), check-merge-conflict, object file detection,\nSwiftLint (SDK + example app), periphery (unused Swift code detection).\n\n---\n\n## Where to look for current in-flight work\n\n`thoughts/shared/plans/` holds active/completed execution plans (including any in-flight\nSDK reorg or migration briefs — e.g. the Electron reorganization plan); `thoughts/shared/issues/`\nholds tracked bug write-ups. Check these directories directly rather than trusting a\nstatus summary written into this file — both change too often for a static snapshot here\nto stay accurate.\n",".claude/skills/sdk-test-starters/SKILL.md":"---\nname: sdk-test-starters\ndescription: Run a REAL end-to-end inference smoke test (model download + load + generate, with visible output) on every RunAnywhere starter app — iOS, macOS/Swift, Android, Web, Electron — after a runanywhere-sdks release. Build success alone is never sufficient evidence.\n---\n# SDK Test Starters\n\nPost-release validation runbook for the five RunAnywhere SDK consumer apps. This\nskill exists because \"it built\" has been reported as \"tested\" before, and that is\nnot evidence of anything except that the compiler ran.\n\n## 0. The evidence bar (applies to every app, no exceptions)\n\nFor each app you must produce, and be able to state explicitly:\n\n1. **Version proof** — print/log the actual resolved SDK version from inside the\n   running app (a startup log line, a version screen, an `swift package show-dependencies`\n   / `./gradlew dependencies` / `npm ls @runanywhere/...` output, etc). Never assume\n   a version bump \"took\" just because you edited a manifest.\n2. **Real model acquired** — a real (small) model downloaded fresh, or confirmed\n   already cached, by the app itself (not a file you hand-placed unless that IS the\n   app's supported flow).\n3. **Load** — the model actually loads (watch for a load-succeeded log/state, not\n   just \"no crash\").\n4. **One real inference call** — LLM `generate()` is almost always the easiest\n   modality to drive; use it unless another modality is genuinely less work for\n   that specific app.\n5. **Visible output** — capture the actual generated text (or audio/image) and\n   quote it in your report. \"It responded\" is not enough — show what it said.\n6. **Logs reviewed** — read the console/log stream for errors and warnings and\n   explicitly explain any that appear. Do not silently drop a stack trace because\n   the app \"still produced output.\"\n\nReporting an app as tested from a build/compile success alone is a skill violation.\nIf you could not get past build, that is a BLOCKED report (see §8), not a PASS.\n\n## 1. Locating each app's real local checkout\n\nThis machine accumulates many differently-named copies of the same repos over time\n(worktrees, experiment clones, old branches, scratch dirs). **Never trust a\ndirectory name.** Before touching anything in a candidate directory:\n\n```bash\ngit remote get-url origin          # must be https://github.com/RunanywhereAI/<repo>.git\ngit status --short --branch        # what branch, what's dirty, is it someone else's WIP?\n```\n\nAs of this writing, the known-good local checkouts for this user are here (verify\nthis each time — do not hardcode it further than \"check this location first\"):\n\n```\n~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-ios       -> RunanywhereAI/runanywhere-ios.git\n~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-android   -> RunanywhereAI/runanywhere-android.git\n~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-web       -> RunanywhereAI/runanywhere-web.git\n~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-electron  -> RunanywhereAI/runanywhere-electron.git\n```\n\nThe macOS/Swift case has no separate consumer repo — see §4, it lives inside\n`~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks` (confirm with\n`git remote get-url origin` there too — this machine also has stray full-monorepo\nclones elsewhere, e.g. under `~/development/hard/ambient/`, that are NOT the one to\nuse unless you've verified it's the intended checkout).\n\nThese starter checkouts are commonly sitting on a feature branch, not `main` — e.g.\nat last check `runanywhere-ios` and `runanywhere-android` were on\n`add-gemma4-qwen3.6-qwen3.8-models` and `runanywhere-web` on `add-supertonic-3-tts`.\nThat is fine to test FROM, but say so in your report, and do not `git checkout` away\nfrom a branch that has uncommitted work you didn't create — if `git status --short`\nshows dirty files you don't recognize, stop and ask rather than stashing/resetting.\n\nIf genuinely unsure which directory is real, `git clone` a fresh one into the\nscratchpad rather than guessing — a wrong guess wastes far more time than a clone.\n\n## 2. Cross-repo facts this skill assumes\n\n- SDK version SoT: `core/VERSION` in `runanywhere-sdks`, bumped everywhere by\n  `scripts/release/sync-versions.sh <version>`.\n- Release publish is `.github/workflows/release.yml` (dispatch or `v*.*.*` tag).\n- **Never** treat a build success against `RUNANYWHERE_USE_LOCAL_NATIVES=1` (or any\n  monorepo-local staged artifact) as proof the *public* release works — see §4.\n- QHexRT/QNN material must never appear in a public artifact — release.yml\n  hard-fails on it. If anything you're testing surfaces a QHexRT asset in a public\n  package, stop and flag it, don't route around it.\n\n## 3. iOS — `runanywhere-ios`\n\nSDK pin lives in `Package.swift`:\n\n```swift\n.package(url: \"https://github.com/RunanywhereAI/runanywhere-swift.git\", from: \"0.20.19\")\n```\n\nThis only resolves once `runanywhere-swift` (the generated Swift-only SPM\ndistribution repo — see `~/.claude/skills/sdk-publish/SKILL.md` §4) is\nactually tagged at the target version. Steps:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-ios\n# bump the `from:` version in Package.swift to the target release, then:\nswift package update            # or open in Xcode and let it resolve\n./scripts/build_and_run_ios_sample.sh simulator \"iPhone 16 Pro\"\n```\n\n`./scripts/verify.sh` is the CI-equivalent gate: it resolves the package remotely\nand runs a full `xcodebuild` against the iOS Simulator — use it to prove the\nresolve is clean (it fails if `Package.resolved` ends up dirty, i.e. the committed\npin was stale). `./scripts/smoke.sh` is a fast, non-compiling grep-based check —\nit does NOT count as evidence for this skill's bar, only `build_and_run` +\nwatching real output does.\n\nDrive one real `generate()` call through the app UI (or the equivalent debug\naction if the sample exposes one) against a small downloaded model, then watch the\nruntime log stream for the version line, the model download/load, the generation,\nand any errors:\n\n```bash\nlog stream --predicate 'subsystem CONTAINS \"com.runanywhere\"' --info --debug\n```\n\nMost loggers use `com.runanywhere.RunAnywhereAI`; a few use plain\n`com.runanywhere`; the SDK logs under its own subsystem — the `CONTAINS` match\ncovers all of them. On a physical device use `idevicesyslog | grep \"com.runanywhere\"`\ninstead.\n\n## 4. macOS/Swift — no separate repo, use the in-tree example or rcli\n\nThere is no `runanywhere-macos` consumer repo. Two in-monorepo options, both inside\n`~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks`:\n\n- `bindings/swift/example/` — a minimal SwiftPM example app/executable.\n- `rcli/` — the C++ core's CLI, `rac_*` C ABI consumer.\n\n**The critical gotcha**: the repo's own `Package.swift` is fail-closed toward the\n*real published release* — it resolves the remote `binaryTargets` unless you\nexplicitly opt into locally staged XCFrameworks with\n`RUNANYWHERE_USE_LOCAL_NATIVES=1`. Every doc/script example in this repo sets that\nenv var (because local dev wants fast iteration against in-progress binaries) —\nbut that means copy-pasting those examples verbatim tests your LOCAL staged build,\nnot the actual public release artifact. To prove the published release works:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks/bindings/swift/example\n# DO NOT set RUNANYWHERE_USE_LOCAL_NATIVES — leave it unset so Package.swift\n# resolves the real remote release binaries for the target version.\nswift build\nswift run runanywhere-minimal \"Name three colours.\"\n```\n\nIf the example was previously built/run with the env var set, `swift build` may\nreuse a stale resolve — run `swift package reset` (or delete `.build/`) first if\nyou're not sure which mode the last resolve used.\n\nFor rcli instead (also validates the real published core):\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks\nrcli models download qwen3          # ~639MB, small enough for a quick smoke test\nrcli llm generate --model qwen3 \"Reply with exactly: RCLI WORKS\" --reasoning off\n```\n\n`rcli models list --all` shows the rest of the built-in catalog\n(`whisper-tiny`, `smolvlm2`, `piper`, …) if LLM generate isn't the right modality\nfor what you're verifying.\n\n## 5. Android — `runanywhere-android`\n\nVersion pin: `gradle/libs.versions.toml` has ONE shared catalog entry:\n\n```toml\nrunanywhere = \"0.20.19\"\n...\nrunanywhere-sdk      = { group = \"io.github.sanchitmonga22\", name = \"runanywhere-sdk\",              version.ref = \"runanywhere\" }\nrunanywhere-llamacpp = { group = \"io.github.sanchitmonga22\", name = \"runanywhere-llamacpp\",          version.ref = \"runanywhere\" }\nrunanywhere-onnx     = { group = \"io.github.sanchitmonga22\", name = \"runanywhere-onnx\",               version.ref = \"runanywhere\" }\nrunanywhere-qhexrt   = { group = \"io.github.sanchitmonga22\", name = \"runanywhere-qhexrt-android\",     version.ref = \"runanywhere\" }\n```\n\n**CRITICAL — do not bump this block as one unit.** `runanywhere-qhexrt-android` is\npermanently frozen at whatever version it was last published at before being\nexcluded from the public Maven train; by policy it can never move forward via\npublic Maven again. Verify the actual frozen version FRESH every time (do not\ntrust a remembered number — it doesn't change, but don't skip the check):\n\n```bash\ncurl -s https://repo1.maven.org/maven2/io/github/sanchitmonga22/runanywhere-qhexrt-android/maven-metadata.xml\n```\n\nThe other three artifacts (`runanywhere-sdk`, `-llamacpp`, `-onnx`) CAN and should\nmove to the new release version. This means the single `runanywhere` catalog ref\nmust be **split into two entries** — one at the new version for sdk/llamacpp/onnx,\none pinned to the frozen qhexrt version — not bumped as a block.\n\n**Before trusting a mixed pin works**: verify `RAC_PLUGIN_API_VERSION` (the\nplugin ABI version constant in the C++ core, exposed identically to Kotlin) is\nunchanged between the old public SDK version and the new one. If it changed, mixing\na newer sdk/llamacpp/onnx with the frozen qhexrt version is NOT safe — this is a\nhuman decision (needs either an ABI-compat shim or dropping qhexrt from this build),\nnot something to force through.\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-android\n./gradlew :app:assembleDebug\n./gradlew :app:installDebug        # installs to a connected emulator/device\nadb logcat | grep -i runanywhere   # or a more specific tag if the app uses one\n```\n\nLaunch the app, drive one real `generate()` call against a small downloaded model\n(the app's own model picker/download flow), confirm the resolved SDK version is\nlogged, and review logcat for errors before reporting a PASS.\n\n## 6. Web — `runanywhere-web`\n\n`package.json` has 4 `@runanywhere/*` deps pinned with a caret range:\n\n```json\n\"@runanywhere/proto-ts\":    \"^0.20.19\",\n\"@runanywhere/web\":         \"^0.20.19\",\n\"@runanywhere/web-llamacpp\":\"^0.20.19\",\n\"@runanywhere/web-onnx\":    \"^0.20.19\"\n```\n\nBump all 4 to the new version, then:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-web\nnpm install\nnpm run build\nnpm run dev      # vite --host localhost --port 3000 --strictPort\n```\n\nThe dev/preview server is what supplies COOP/COEP headers required for\n`SharedArrayBuffer` — WASM inference will silently fail (or refuse to init\nthreads) without them, so don't swap in a bare static file server.\n\n`~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks/bindings/web/AGENTS.md`\nhas the authoritative Validation section (its own heading is `## Validation`) —\nfull checklist:\n\n1. Fresh browser context.\n2. Example app served with COOP/COEP headers.\n3. Model download.\n4. Model load.\n5. Real browser inference for the target modality.\n6. Logs/screenshots reviewed.\n\nIts \"Required release gates\" (run before browser validation) from the same repo:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks/bindings/web\nnpm run typecheck && npm run lint && npm run test && npm run build\nnpm run test:browser\nnpm run test:browser:release\ncd example && npm run typecheck && npm run build\n```\n\nStatic gates passing is not the smoke test — you still need an actual browser\nwatching real inference happen against the starter app. Prefer a real browser\nautomation tool (`claude-in-chrome` MCP tools, or Playwright — the maintained\nsuites under `bindings/web/tests/browser/` already use `playwright.config.ts`)\nover curling the served page: WASM inference genuinely needs a browser JS engine.\nLoad the served starter app, download/load a small model, trigger generate, and\nread the actual rendered output text plus the browser console — do not infer\nsuccess from an HTTP 200 on the page load.\n\n## 7. Electron — `runanywhere-electron` (SPECIAL CASE)\n\n**Do not assume a fresh SDK publish is available for this app.** Electron's own\nnpm packages (`@runanywhere/electron`, `-llamacpp`, `-onnx`, `-sherpa`, `-qhexrt`,\n`-neurt`) are on a SEPARATE, manual-publish pipeline — NOT part of `release.yml`\n(grep confirms zero mentions of these package names there). This separation is a\n**confirmed, deliberate** repo-owner decision, not a gap. Check\n`~/.claude/skills/sdk-publish/SKILL.md` §6 before assuming anything moved.\nElectron is **no longer WIP on a branch** — the old \"in-progress on\n`smonga/electron_upgrade`\" note is gone from the root `AGENTS.md`/`CLAUDE.md`\nentirely as of this checkout; `bindings/electron/` is a fully merged SDK on `main`.\nAs of `fix/electron-cicd-staging-url-npu-ane`,\n`.github/workflows/electron-native-package.yml` builds every backend for real and\npackages every tarball — but still does not publish; verify against a live run\nrather than assuming build automation status either way.\n\nTest the starter against whatever version it is CURRENTLY pinned to unless a human\nhas explicitly confirmed a newer Electron publish is ready and intended:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-electron\ncat package.json | grep '@runanywhere/electron'   # confirm current pin before touching it\n```\n\n**`@runanywhere/electron-qhexrt` being public on npm is a CONFIRMED, settled\npolicy decision** (repo owner, 2026-08-20), not a contradiction to flag — it\ncontains real Hexagon NPU binaries (`libQnnHtpV81Skel.so`, `QnnHtp*.dll`,\n`rac_commons.dll`, `runanywhere_qhexrt.dll`) on purpose. The QHexRT-stays-private\npolicy enforced elsewhere in this repo family is specifically about the\nGitHub-Release-side asset set (Android etc.), a different channel. Only re-raise\nthis if you find evidence the policy changed.\n\nTwo layers to actually exercise, do not conflate them:\n\n**(a) Cross-platform Electron JS + macOS native addon** — buildable and testable\nlocally on this Mac right now:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-electron\nnpm install\nnpm run build   # or the app's documented dev/start script — check package.json scripts\nnpm start        # or equivalent — launch the actual Electron app\n```\n\nDrive one real `generate()` call in the running app, watch its console/log output\nfor the resolved SDK version, model download, load, and generation, and read any\nerrors rather than ignoring them.\n\n**(b) Genuine Windows-native behavior** — a macOS build proves NOTHING here\nregardless of how clean the cross-platform code looks; the native addon and any\nWindows-only DLLs must be exercised on real Windows.\n\nThe team keeps a **Windows ARM64 test machine reachable over SSH**. Its hostname,\nuser, key, and VPN/mesh node name are operator-specific and deliberately not\nrecorded in this repo — read them out of the operator's own `~/.ssh/config` (look\nfor the Windows/ARM64 host entry) and use that alias below wherever `$WIN_BOX`\nappears. If no such entry exists, the box is not configured for you: say so and\nask, rather than guessing at a hostname.\n\n```bash\nWIN_BOX=<the ssh alias from ~/.ssh/config>\n```\n\n**Always check reachability first, never just try SSH and retry on timeout.** The\nbox lives on a private mesh VPN and is frequently asleep, so start by asking the\nVPN client whether the node is up (e.g. `tailscale status`, matching on the host\nentry's `HostName`) rather than opening a connection.\n\nIf the node reports `offline, last seen ...`, the box is asleep/powered off.\n**A connection timeout here means \"device asleep,\" not \"config is wrong.\"** Tell\nthe human and ask them to wake it; do not loop retrying SSH. Only once the node\nshows a live/idle state, confirm with:\n\n```bash\nssh -o ConnectTimeout=8 \"$WIN_BOX\" \"whoami\"\n```\n\nThen, with the repo already cloned there:\n\n```bash\nssh \"$WIN_BOX\" \"cd <repo-path-on-that-box>; git pull; npm install; npm run build\"\n# then run whatever launches the Windows Electron build and drive a real generate call\nscp \"$WIN_BOX\":<remote-log-or-artifact-path> ./local-copy   # pull back evidence to inspect\n```\n\nDo not claim Windows-specific validation happened unless you actually obtained a\nshell on that box and ran something real there — a clean macOS build is not\nsubstitute evidence.\n\n**`node_modules/electron` on this box silently tracks whichever arch last ran `npm\ninstall`, not which Node launches the test.** The x64 (llamacpp/onnx/sherpa) and\narm64 (QHexRT) lanes share ONE `runanywhere-electron` checkout/`node_modules` tree.\nPrepending an x64 Node to `PATH` and running `npx playwright test` does NOT make\nElectron itself x64 — `require('electron')` always resolves the single installed\n`node_modules/electron/dist/electron.exe`, and whichever arch that binary actually\nis determines which `prebuilds/win32-<arch>/` directory the running app looks in,\nregardless of `scripts/use-sdk.mjs local`'s staging (which itself is arch-aware\nbased on the *launching* Node, so it can correctly report \"staged win32-x64\" while\nthe process that will actually run is arm64 — a silent mismatch). Verify with:\n`powershell -Command \"$p=[IO.File]::ReadAllBytes('...\\electron.exe'); $peOff=\n[BitConverter]::ToInt32($p,60); [BitConverter]::ToUInt16($p,$peOff+4)\"` — `43620`\n(`0xAA64`) is ARM64, `34404` (`0x8664`) is x64. If it's the wrong arch (e.g. a prior\nsession's `npm install` for the other lane silently overwrote it), a genuine re-test\nof the other lane needs a FULLY SEPARATE checkout/`node_modules` tree installed\nunder the matching-arch Node — don't assume PATH-prepending alone is sufficient, and\ndon't trust `use-sdk.mjs status`'s \"platform: win32-x64\" output as proof the running\nElectron process is actually x64.\n\n**NPU hardware is a scarce, single-owner resource — serialize, never parallelize.**\nThis box has ONE physical Hexagon NPU. `engines/qhexrt/qhexrt_session.cpp`'s\n`session_open()` has a load-bearing comment on this exact failure mode: the HTP\nexposes a small number of protection domains with a per-PD context ceiling, so a\n*second* concurrent model load fails with `RAC_ERROR_BACKEND_INIT_FAILED`\n(\"Backend initialization failed\") — a fast (tens-of-ms), generic-looking failure\nthat is easy to mistake for a real regression. Confirmed once tonight: two agents\neach independently exercising QHexRT on this box at the same time produced exactly\nthis failure. Before concluding a QHexRT test genuinely failed, confirm nothing\nelse (another agent, a leftover Electron process, a stuck previous test run) is\nalso holding an NPU session open on this box — check for lingering `electron.exe`/\n`node.exe` processes from a prior run and kill them if orphaned, and never launch\ntwo NPU-touching verification passes concurrently on this machine.\n\n**\"Backend initialization failed\" has (at least) three distinct root causes —\ndon't stop at PD exhaustion.** Confirmed on 2026-08-17 after PD-exhaustion and\nstaleness were both ruled out (fresh rebuild, source-identical prebuilt, no other\nprocess holding a session): the *actual* bug was two compounding issues, both now\nfixed/documented —\n1. **Real source bug, fixed in PR #733**: `qhexrt_session.cpp`'s `runtime_acquire()`\n   called `qhx_runtime_create(nullptr, nullptr)` on every non-Android platform,\n   trusting the OS default DLL search order to find `QnnHtp.dll`/`QnnSystem.dll`.\n   On Windows that order never includes a dynamically-loaded sibling plugin's own\n   directory (`LOAD_WITH_ALTERED_SEARCH_PATH`, used to load `runanywhere_qhexrt.dll`\n   itself, only widens the search for *that* load call, not later `LoadLibrary`\n   calls made by code already running inside it) — so the QNN DLLs were invisible\n   even sitting right next to the plugin. Fixed by resolving the plugin's own\n   module directory (`GetModuleHandleExA`+`GetModuleFileNameA`) and passing\n   explicit paths, mirroring Android's existing `ADSP_LIBRARY_PATH` resolution in\n   the same file.\n2. **Operational staleness, NOT a script bug**: even after (1), the QNN runtime\n   DLLs actually staged into `@runanywhere/electron-qhexrt`'s\n   `prebuilds/win32-arm64` (`node_modules/.../electron-qhexrt/prebuilds/win32-arm64/`)\n   were from an old QAIRT build (~2.45.x vintage, Dec 2025 files) while the box's\n   installed QAIRT was 2.48.0.260626 — a real ABI-relevant version mismatch.\n   `bindings/electron/scripts/bundle-native.ts`'s `stageQnnRuntime()` just mirrors\n   whatever `RA_QNN_RUNTIME_DIR` points at with **no version check** — it's\n   correct-as-written; someone had pointed it at a stale neurun-side\n   `qairt-runtime-248`-named folder that was never refreshed. Fix: rebuild that\n   flat dir from the CURRENT QAIRT SDK's `lib/aarch64-windows-msvc/` (host DLLs:\n   `QnnHtp.dll`, `QnnHtpPrepare.dll`, `QnnHtpV81Stub.dll`,\n   `QnnHtpV81CalculatorStub.dll`, `QnnSystem.dll`) + `lib/hexagon-v81/unsigned/`\n   (DSP side: `libQnnHtpV81Skel.so`, `libqnnhtpv81.cat`), point\n   `RA_QNN_RUNTIME_DIR` at it, and re-run\n   `npm run bundle:native -- --package=qhexrt` from `bindings/electron/`.\n   **The already-published `@runanywhere/electron-qhexrt` npm package (as of\n   0.20.24) almost certainly ships the same stale DLLs** — this needs a\n   human decision (republish/patch) before claiming the published package works\n   on Windows NPU, separate from the source fix in PR #733.\n3. **PD exhaustion** (the pre-existing entry above) remains real and still applies\n   when two sessions genuinely contend for the one physical NPU — but it is not\n   the only explanation, and the log line to actually check first is the native\n   one: subscribe via `window.runanywhere.logging.records()` (after\n   `logging.setDebugMode(true)`) or add a temporary main-process log capture —\n   `app.process().stdout`/`stderr` do NOT carry it, because the addon runs in a\n   separate `utilityProcess.fork()` host, not the Electron main process, and\n   `RAC_LOG` is not an env var the logger reads (it does nothing). The generic\n   `[QHexRT] qhx_runtime_create failed (QNN libs unavailable?)` vs.\n   `qhx_model_load failed: ... N NPU model(s) already resident` log text is what\n   actually distinguishes cause (1)/(2) from cause (3) — always get that line\n   before diagnosing further.\n\n**Workflow gotcha:** if you orchestrate this via the Workflow tool, its agent()\ncalls do NOT reliably block-wait on a long-running remote build the way a direct\npoll loop does — observed once tonight where a workflow's own agents spawned an\ninternal Monitor/poller for the build log and then returned immediately,\nreporting \"still waiting,\" which let the whole workflow finish with nothing\nactually verified. For \"wait N tens-of-minutes for a real native build to\nfinish,\" use the top-level Monitor tool yourself (a shell poll loop over SSH\nchecking for a terminal BUILD_SUCCEEDED/BUILD_FAILED marker line) rather than\ndelegating the wait itself into a workflow agent's own judgment.\n\n### The Windows box's actual layout, and why it's more than a cross-platform check\n\n**This is a Hexagon-NPU-equipped Windows machine** (Snapdragon-class ARM Windows\ndevice, Copilot+ PC), not just a generic x86 Windows box. That makes it the one\nplace a QHexRT/Hexagon backend build+test can produce real device-only truth for\nWindows — matching the parent workspace's own \"x86 is a simulator, device-only\ntruth\" rule (see the top-level `Qualcomm/CLAUDE.md` and `QHexRT/CLAUDE.md`). Don't\ntreat this box as merely a Windows Electron smoke-test target; it's the only real\nHexagon NPU test rig reachable for this platform.\n\nPer the user (2026-08-17), under `~/Downloads/RunAnywhere/` on that box there are\nthree sibling repo checkouts at the same level — **verify each on connect, don't\nassume paths without checking, since this is relayed secondhand and may have\ndrifted**:\n- `runanywhere-sdks` — this monorepo, presumably already cloned; `git fetch && git\n  log -1 origin/main` to see how far behind it is before doing anything.\n- `runanywhere-electron` — the Electron starter app (same repo as\n  `RunanywhereAI/runanywhere-electron` elsewhere in this skill).\n- a third repo referred to verbally as \"the neuron/new-run inference engine for the\n  Hexagon NPU\" — almost certainly **`neurun`**, the QHexRT/NeuRT split-out repo\n  (see memory: \"neurun component split merged — QHexRT/ + NeuRT/ + shared/, root is\n  a closed 20-entry set enforced by a test\"). Confirm the actual directory name and\n  remote (`git remote get-url origin`) on connect rather than guessing from the\n  transcription.\n\n**Full task on this box (per explicit user instruction, not just a smoke test):**\n1. `git pull` (or fetch+fast-forward) all three repos to latest `main`.\n2. Rebuild the QHexRT/Hexagon NPU backend from `neurun` against this device's real\n   Hexagon NPU — this is the one environment where that build+run is meaningful\n   proof, not a simulator claim.\n3. Bump Electron's native dependencies to their latest versions — llama.cpp,\n   sherpa-onnx, ONNX Runtime, and whatever else `bindings/electron` pins — across\n   the repos involved. Treat this as a real dependency-bump task (check for\n   breaking API changes, don't just edit a version string and assume it builds).\n4. Update the model catalog for **all 5 starter apps across all 4 consumer repos**\n   (iOS, Android, Web, Electron — plus whatever the \"5th app\" split means for\n   macOS/Swift, see the rest of this skill) so the catalog is consistent everywhere,\n   not just on this Windows box.\n5. Run a real end-to-end inference pass on this box specifically (not just build\n   success) — both the general Electron/llama.cpp path AND the Hexagon NPU path via\n   `neurun`/QHexRT.\n6. Open PRs for whatever changed, in each affected repo. Do not merge them —\n   the user explicitly said they'll review.\n\nThis is real engineering work (native dependency bumps + an NPU backend rebuild),\nnot a mechanical version-string edit — expect it to take real time and to\ngenuinely need `QHexRT/CLAUDE.md` and `neurun`'s own docs read in full before\ntouching that backend, per the parent workspace's own instructions.\n\n## 8. Reporting\n\nFor each of the 5 apps (iOS, macOS/Swift, Android, Web, Electron), state exactly\none of these two outcomes — never blur them:\n\n- **FULLY VERIFIED** — with concrete evidence: the version string actually\n  resolved/logged, the model used, and the actual output produced (quote it).\n- **BLOCKED** — with the exact concrete reason, e.g.: the Windows ARM64 box's\n  mesh-VPN node is offline; Android emulator unavailable in this environment; App Store /\n  notarization credential missing; `runanywhere-swift` not yet tagged at target\n  version so `swift package update` can't resolve; electron-qhexrt policy conflict\n  needs a human call before proceeding further.\n\nA clearly reported partial/blocked result is far more valuable than an optimistic\nsummary that overstates what was actually observed. If you hit the known Electron\nqhexrt npm-publish policy conflict (§7), call it out in the report even if it\nwasn't the thing blocking your specific test — it's a standing issue every release\nnight should re-surface until a human resolves it.\n\n## Credentials referenced but never printed by this skill\n\nThese exist and are checked for presence only — never print their values:\n\n- Maven Central: `mavenCentral.username` / `mavenCentral.password`,\n  `signing.gnupg.keyName` / `signing.gnupg.passphrase` in `~/.gradle/gradle.properties`.\n- GPG signing key `CC377A9928C7BB18` — check with\n  `gpg --list-secret-keys --keyid-format LONG`.\n- macOS notarization profile `runanywhere-notary` — check with\n  `xcrun notarytool history --keychain-profile runanywhere-notary`; codesign\n  identity `Developer ID Application: RunAnywhere, Inc` — check with\n  `security find-identity -v -p codesigning`.\n- npm — `npm whoami` should already show a logged-in user.\n\nNone of the app-level smoke tests in this skill require touching these directly;\nthey matter only if a test path requires a fresh distro-repo publish (e.g.\n`runanywhere-swift` not yet tagged) — that's a separate concern of the\n`sdk-publish` skill, not this one.\n","CLAUDE.md":"# AGENTS.md\n\nGuidance for AI coding assistants working in this repository.\n\n> **`AGENTS.md` is the real file; each `CLAUDE.md` is a committed symlink to the\n> `AGENTS.md` beside it.** Editing either name edits the same bytes. If you create a new\n> `AGENTS.md` in a directory that doesn't have one, run\n> `bash scripts/validation/gates/check_agents_claude_sync.sh --fix` from the repo root to\n> create its `CLAUDE.md` symlink — `pr-build.yml` fails any tracked `AGENTS.md` missing one.\n> Never hand-create a `CLAUDE.md` as a real file.\n\n## Working conventions\n\n- **Focus on simplicity.** Clean, SOLID-ish separation of concerns; reuse over\n  reinvention. No mock implementations and no new unit tests unless the task explicitly\n  asks for them.\n- **Plan first for non-trivial work.** Write the plan to\n  `thoughts/shared/plans/{descriptive_name}.md`, get it approved before implementing, and\n  keep it updated (append what changed) as you go so the next engineer can pick it up.\n- **Structured types over raw strings.** Enums/sealed classes/data classes for\n  configuration and results — this is enforced per-SDK by lint (ktlint/detekt, SwiftLint,\n  ESLint) so ad hoc strings tend to get caught anyway.\n- **Read files fully** before editing them; only use offset/limit on files too large to\n  read at once.\n- **Use full local build parallelism** (`-j \"$(sysctl -n hw.logicalcpu)\"` / equivalent),\n  scaling down only under real memory/thermal pressure — not because load average looks\n  high.\n- **Multi-agent workflows run on Sonnet.** When fanning work out to subagents (the\n  `Workflow` tool, or several parallel `Agent` calls), pass `model: 'sonnet'` on every\n  `agent(...)` call rather than letting them inherit the session model. A fan-out of\n  8-10 probes reading large files is exactly the shape where Sonnet is the right\n  cost/latency trade, and these agents are doing bounded, well-specified retrieval and\n  analysis rather than open-ended reasoning. Reserve the session's stronger model for\n  the synthesis step that consumes their reports, and for anything where a wrong answer\n  is expensive and hard to detect — a final design decision, an adversarial verification\n  pass, or a security-relevant judgement.\n\n  ```js\n  // in a Workflow script\n  const findings = await parallel(LENSES.map((l) => () =>\n    agent(l.prompt, { label: `probe:${l.key}`, schema: FINDINGS, model: 'sonnet' })))\n\n  // the synthesis that reads them can stay on the session model\n  const plan = await agent(synthesisPrompt, { schema: PLAN, effort: 'high' })\n  ```\n\n## The most important architectural rule: logic lives at the lowest layer that serves everyone\n\nEach feature/modality (LLM, STT, TTS, VAD, VLM, RAG, LoRA, Voice) is invoked through\n**one** SDK entry point; the SDK, and below it C++ commons, does all the heavy lifting. If\nan example app builds a multi-step bootstrap sequence, hardcodes a model/engine constant,\nor post-processes model output, that is a bug in the SDK, not the app — fix it down a\nlayer.\n\n1. **C++ commons (`core/`)** — cross-platform, non-I/O logic: model lifecycle, registry,\n   download orchestration, RAG session management, inference routing. All 5 SDKs get the\n   fix for free.\n2. **Platform SDK layer** — platform-specific I/O or runtime bridging only (Web OPFS,\n   iOS Keychain, Android Keystore, WASM MEMFS mirroring).\n3. **Example apps** — UI rendering, navigation, thin SDK calls. No business logic, no\n   internal-SDK knowledge (path patterns, framework→directory mappings), no workarounds.\n\n**iOS is the source of truth.** When behavior is ambiguous in any other SDK, check the\niOS Swift implementation first and copy the logic exactly, adapting only syntax.\n\n---\n\n## Repository overview\n\nCross-platform on-device AI SDK monorepo. A single C/C++ core (`runanywhere-commons`,\n~118K first-party LOC plus ~420K generated proto bindings) implements all AI business\nlogic behind a pure C ABI (`rac_*` prefix). Platform SDKs are thin bridges that supply\nplatform services (file I/O, HTTP, Keychain, audio) via an inversion-of-control struct and\ncall into the C core for all inference. Protobuf IDL schemas generate type-safe bindings\nfor every language.\n\n**Current version**: `0.20.37` (canonical source: `core/VERSION`)\n\n| SDK | Path | Bridge mechanism | Platforms |\n|-----|------|-------------------|-----------|\n| Swift | `bindings/swift/` | XCFramework + CRACommons module map | iOS 17.5+, macOS 14.5+ |\n| Kotlin | `bindings/kotlin/` | JNI (`librunanywhere_jni.so`) | Android (min API 24) |\n| Flutter | `bindings/flutter/` | Dart FFI | iOS, Android |\n| React Native | `bindings/react-native/` | NitroModules (JSI HybridObject) | iOS 17.5+, Android arm64 |\n| Web | `bindings/web/` | Emscripten WASM + TypeScript | Chrome, Safari, Firefox |\n| Electron | `bindings/electron/` | Node-API addon | Windows x64 (preview) |\n| Python | `bindings/python/` | pybind11 extension (`runanywhere._core`) | Windows, macOS, Linux (alpha) |\n\nEach has its own `AGENTS.md` with full architecture, build commands, and conventions —\nread it before working in that SDK.\n\n| Native dir | Contents |\n|-----------|----------|\n| `core/` | C/C++ core library: all AI logic, plugin registry, event system |\n| `engines/` | 7 backend plugins: llamacpp, sherpa, onnx, cloud, mlx, qhexrt, neurt |\n| `runtimes/` | 3 runtime adapters: cpu (always), onnxrt, coreml |\n| `idl/` | 39 Protobuf schemas + per-language codegen scripts |\n\n### Consumer apps live in separate repos\n\nThe four full consumer apps were extracted into standalone repos (history preserved); PR\nagainst them there, not here: [runanywhere-ios](https://github.com/RunanywhereAI/runanywhere-ios),\n[runanywhere-android](https://github.com/RunanywhereAI/runanywhere-android),\n[runanywhere-web](https://github.com/RunanywhereAI/runanywhere-web),\n[runanywhere-electron](https://github.com/RunanywhereAI/runanywhere-electron).\n\nTwo example apps remain in-tree (`bindings/flutter/example/`, `bindings/react-native/example/`).\nAll example apps share brand orange `#FF6900`, documented in `docs/DESIGN_GUIDELINE.md`.\n\n### Minimal examples are how you verify an SDK change locally\n\nEach in-repo example consumes its SDK from local source (no staging/publishing needed to\nsee an edit): Swift via `RUNANYWHERE_USE_LOCAL_NATIVES=1`, Kotlin via a Gradle composite\nbuild (`includeBuild` + `dependencySubstitution`), Web via Vite aliases into\n`packages/*/src`. They're deliberately minimal (one prompt in, one streamed completion\nout) — feature-complete UI belongs in the consumer repos above.\n\n---\n\n## Cross-platform architecture\n\n`idl/*.proto` is the schema root; `idl/codegen/generate_all.sh` emits per-language\nbindings (Swift, Kotlin/Wire, TS/ts-proto, Dart), all generated and none committed — see\n[`docs/reference/generated-code-contract.md`](docs/reference/generated-code-contract.md)\nfor the full \"what generates what, when\" contract before touching `idl/`.\n\nAll SDKs reach `runanywhere-commons` through the `rac_*` C API. Commons holds the\ncomponent layer (lifecycle), the service layer (dispatch), and the plugin registry, and\nreaches engines through `rac_engine_vtable_t`.\n\n| Engine | Primitives |\n|---|---|\n| llamacpp | LLM, VLM |\n| sherpa-onnx | STT, TTS, VAD |\n| onnx | Embed, Segment |\n| qhexrt | Hexagon NPU |\n| neurt, cloud | Apple Neural Engine, HTTP |\n\nBackend base priorities (highest wins per primitive via `rac_plugin_find()`, no\nruntime/format scoring): qhexrt=150 (QNN-context models only), mlx=110 (Apple),\nllamacpp=100, sherpa=90, onnx/cloud=50. An explicit engine name is honored through\n`rac_plugin_find_for_engine()` regardless of priority.\n\n### Non-obvious invariants that span every SDK\n\n- **Platform adapter IoC**: `rac_platform_adapter_t` is a flat C struct of function\n  pointers populated by each SDK before `rac_init()`. C++ never calls platform APIs\n  directly — file I/O, HTTP, Keychain, logging, memory queries all pass through it.\n- **Two-phase init**: every SDK does Phase 1 (sync: register adapter, load native libs,\n  configure logging) then Phase 2 (async: authenticate, register device, fetch model\n  assignments, discover downloaded models).\n- **Plugin ABI**: `RAC_PLUGIN_API_VERSION = 9u`; a version mismatch is an immediate\n  rejection. Each vtable has 10 active primitive slots and 7 reserved. NULL slot = not\n  supported. `rerank_ops` (wire value 11) was promoted from `reserved_slot_2` in ABI v8;\n  wire value 6, retired in ABI v4, stays permanently retired — never reuse it. See\n  `core/AGENTS.md` for the full ABI history and how to add a primitive.\n- **Static vs. dynamic plugins**: iOS and WASM force `RAC_STATIC_PLUGINS=ON` (no\n  `dlopen`), registered via `RAC_STATIC_PLUGIN_REGISTER(name)` + `-force_load` /\n  `--whole-archive`. Android/Linux/macOS default to dynamic loading via\n  `rac_registry_load_plugin()`.\n- **Streaming fan-out**: C++ allows only one proto-byte callback per component handle.\n  Each SDK implements its own fan-out to multiplex that into multiple subscribers (Swift\n  `AsyncStream`, Kotlin `Flow`, Dart `StreamController`, TS `AsyncIterable`).\n- **Proto types are canonical**: never hand-write enum values or structured types that\n  exist in `idl/*.proto` — use the generated types/typealiases and regenerate instead.\n- **HTTP is platform-provided**: no libcurl. Each SDK registers a\n  `rac_http_transport_ops_t` (URLSession on Apple platforms, OkHttp on Android,\n  `emscripten_fetch` on Web).\n- **NeuRT crosses a repo boundary as BYTES, not source**: the Apple engine is built and\n  published by the private `neurun` repo and consumed here as prebuilt archives, pinned by\n  tag + SHA-256 in `core/VERSIONS` (`NEURT_*`). Fetch with `scripts/build/download-neurt.sh`.\n  Each archive carries a receipt naming the `RAC_PLUGIN_API_VERSION` it was built against,\n  and both the downloader and CMake refuse a mismatch — a vtable-layout change relinks\n  cleanly and corrupts dispatch at runtime, so nothing else would catch it.\n\nA side-by-side comparison of entry point / bridge / streaming / events / storage / HTTP\nper SDK — useful when porting a fix across SDKs — lives in\n[`docs/reference/cross-sdk-parity.md`](docs/reference/cross-sdk-parity.md).\n\n---\n\n## Building and running\n\nThe root `CMakeLists.txt` (version from `core/VERSION`) is the single entry point for\nnative builds; `CMakePresets.json` defines `macos-{debug,release}`, `linux-{debug,release,asan}`,\n`ios-{device,simulator}`, `android-arm64`, `wasm`, `cpp-desktop-*`, and the `windows-*` presets.\n\n```bash\ncmake --preset macos-debug && cmake --build build/macos-debug && ctest --preset macos-debug\n```\n\n**`./run <group> [subcommand]`** is the unified wrapper for everything else — build,\nlint, and run any SDK or example app from one CLI (`./run --help` for the full menu):\n\n```bash\n./run doctor                    # scan host toolchains, show what's buildable here\n./run setup                     # provision env for every host-buildable target\n./run sdk commons build-android # build C++ commons for all Android ABIs, stage .so files\n./run sdk kotlin build          # (etc. — see per-SDK AGENTS.md for direct tool commands)\n./run example android install   # build + install + launch the Kotlin minimal example\n./run codegen                   # regenerate every language binding from idl/\n```\n\nFor the direct `swift build` / `./gradlew` / `melos` / `yarn` / `npm` commands each SDK\nuses under the hood, and its test/lint/publish targets, see that SDK's own `AGENTS.md`\n(`bindings/<name>/AGENTS.md`) — the commands are kept there, not duplicated here, so they\ncan't drift.\n\nCross-platform build scripts worth knowing about at the root level:\n\n```bash\n./bindings/swift/scripts/build-core-xcframework.sh   # also syncs into RN/Flutter plugin dirs\n./scripts/build/build-core-android.sh                # .so for all ABIs → every SDK's jniLibs/\n./bindings/web/scripts/build-core-wasm.sh\n./scripts/setup/setup-toolchain.sh                   # protoc/wire/ts-proto toolchain\n./idl/codegen/generate_all.sh [--only <lang>]         # swift|kotlin|dart|ts|cpp|python\n```\n\n---\n\n## Version management and releases\n\nCanonical version: `core/VERSION` (single-line semver). Bump everywhere with:\n\n```bash\n./scripts/release/sync-versions.sh <version>\n```\n\nThe full release runbook (version bump → PR → tag → GitHub Release → npm/Maven/pub.dev/\nSwiftPM publishing → cutting the `runanywhere-swift` SPM distribution repo → starter-app\nsmoke tests) is a multi-step process captured in three skills — use them rather than\nimprovising the steps: **sdk-release** (version bump through published GitHub Release),\n**sdk-publish** (registry publishing + the `runanywhere-swift` dist repo cut), and\n**sdk-test-starters** (post-release device/app smoke tests).\n\n### Release pipeline lessons (v0.20.29 through v0.20.33 — read before cutting a release by hand)\n\n- **`native_ios` and `native_android` always rebuild fresh archives on every `release.yml`\n  run — there is no reuse/skip mechanism for either job** (unlike `native_web`'s\n  `reuse_native_web_run_id` or the cpp-desktop kits' `reuse_cpp_desktop_run_id`). This makes\n  the Apple/Android TEMP-pin convention (Package.swift's `sdkVersion`, the Flutter podspecs'\n  `asset_version`, Kotlin's `nativeLibVersion` pinned to an old version \"because this release\n  ships no new iOS/Android archives\") **structurally unsound for any release where those two\n  jobs actually run — which is every release.** It surfaced as three separate bugs\n  (`auto-tag.yml`'s coherence check, `package-sdk.sh`'s podspec check, and `sync-checksums.sh`'s\n  publish-time verification) before the pattern was recognized. If a release genuinely ships\n  no new Apple/Android bytes, that has to be proven by diffing the actual built artifacts\n  against the previous release, not assumed from what changed in the diff.\n- **`RunAnywhereMLXRuntime`'s stripped Mach-O is not byte-reproducible across builds**\n  (`RAC_CHECKSUM_SKIP=RunAnywhereMLXRuntime` in `release.yml`'s publish job exists because of\n  this, and is the ONLY archive exempted from the publish-time checksum gate). The corollary\n  that bit v0.20.29: **the checksum committed to `Package.swift` / the Flutter MLX podspec\n  can only be computed from a build that already happened, but the tag that freezes those\n  files has to be pushed before `native_ios` (which produces that exact artifact) even runs**\n  — so re-syncing checksums against an EARLIER, superseded build (e.g. after a tag had to be\n  moved and re-dispatched to fix an unrelated bug) commits a value that does not match what\n  the FINAL successful run actually publishes, and the skip means nothing catches it. v0.20.29\n  shipped this way (committed `d942788f...`, actually published `fc4b23a0...`) — caught only\n  because the Flutter `runanywhere_mlx` package hadn't been published to pub.dev yet for that\n  version. Before publishing any package that ships `RunAnywhereMLXRuntime`, re-run\n  `bindings/swift/scripts/sync-checksums.sh` against the artifacts from the SPECIFIC run whose\n  tag is being published — never reuse a checksum computed against an earlier attempt.\n- **The Electron QHexRT backend never set `ADSP_LIBRARY_PATH`, and one comment in the\n  codebase confidently asserted Windows doesn't need it.** It does, for exactly one decode\n  path: `QHexRT/src/bonsai/fastrpc_win.cpp`'s custom FastRPC skel (the ternary/Bonsai\n  decoder, e.g. `qwen3.8-27b-1bit-npu`) resolves through `ADSP_LIBRARY_PATH ∪ cwd`, separately\n  from the standard QNN HTP graph path's `LoadLibraryW` + PATH search that `bridge.ts`'s\n  `addSidecarDirToDllSearch` already handled. Every OTHER QHexRT model worked fine in the\n  packaged app, which is exactly what let this ship undetected — validate the Bonsai/ternary\n  path specifically, not just \"some NPU model works.\" Fixed in `bridge.ts`'s\n  `addSidecarDirToDspSearchPath` (#803).\n- **`package-private-engine-overlay.sh`'s QAIRT-runtime copy step filtered on `{\".dll\",\n  \".lib\"}` only**, silently dropping every `.so`/`.cat` skel file (both the standard HTP skel\n  and, since it never read `engines/qhexrt/prebuilt/current/dsp/win-arm64/` at all, the\n  Bonsai skel too). This meant **RCLI could not have run any QHexRT model** — standard or\n  ternary — from a fresh private overlay, a gap invisible until someone actually tried to run\n  inference rather than just building. `bindings/electron/scripts/bundle-native.ts` already\n  had the correct pattern (copy `dsp/win-arm64/` flat, no extension filter); the overlay\n  script just never matched it. Fixed in #804.\n- **A new NeuRT archive must be added in FOUR places, and three of them only fail in a build\n  configuration your Mac never produces.** v0.20.33 added `libneurt_rac_tts_ops.a` and took two\n  wasted release candidates to land, because each site fails somewhere different and late:\n  `engines/neurt/CMakeLists.txt` (probe list + imported target + `_neurt_extra_links`),\n  `build-core-xcframework.sh` (pre-flight guard AND `_NEURT_ROUTABLE_ARCHIVES` — two lists in one\n  file), `package-private-engine-overlay.sh`, and the `#if RAC_NEURT_ROUTABLE` guard around the\n  vtable entry in `rac_plugin_entry_neurt.cpp`. Miss the force-load lists and every native job goes\n  green while `validate_consumer_flutter` dies an hour later on `Undefined symbol:\n  _g_neurt_tts_ops` — the engine registers, all CMake targets compile, and only a CONSUMER's link\n  fails. Miss the vtable guard and the routable build is fine while every PR job WITHOUT\n  `NEURUN_TOKEN` fails, because a non-routable shell still references a symbol nothing defines.\n  A dev machine with the payload staged is routable, so it reproduces neither. `libneurt_rac_*.a`\n  archives now fail packaging by name if they are not in the list\n  (`neurt_assert_no_unlisted_archives`); the vtable guard has no such mechanism — audit all ten\n  slots when adding one. Reproduce a non-routable build by moving `core/third_party/neurt` aside.\n- **`reuse_native_web_run_id` is SAME-VERSION-ONLY, whatever the diff looks like.** `racommons.wasm`\n  embeds `RAC_VERSION_STRING` and `validate_public_packages.py` asserts it against the version being\n  packaged, so reusing ANY prior release's run fails `sdk_web` with *\"does not embed\n  RAC_VERSION_STRING\"*. Worse than a red job: `native_web` is skipped, so that candidate never\n  uploads a web artifact and can never serve as `publish_from_run_id`. Only a re-dispatch at the\n  same version qualifies.\n- **Verify checksums by NAME as well as by URL — the manifests use two different shapes.** The root\n  `Package.swift` interpolates `\\(sdkVersion)` into each URL; the Flutter manifests bind a checksum\n  to a `name:` through a helper with no URL at the call site at all. A URL-only cross-check silently\n  validates 9 of 15 sites and reports success. Re-hash every site against the zip it names, and\n  remember `RAC_CHECKSUM_SKIP` hides `RunAnywhereMLXRuntime` from `publish` entirely, so the Flutter\n  MLX podspec is verifiable only by hand.\n- **A green `rcli backends --json` proves nothing against the OSS kit.** NeuRT ships as a separate\n  private pack, so the public bottle lists no `neurt` at all — a routability check there passes\n  cleanly and means nothing. Apply the private overlay\n  (`cpp-desktop-macos-arm64-neurt-private` workflow artifact) before believing the result.\n- **General pattern across all four of the above**: each one was invisible to the checks that\n  existed (a version-coherence check, a checksum-skip escape hatch, a \"the model loaded\"\n  smoke test, a \"the build succeeded\" CI job) because none of those checks actually ran the\n  specific thing that was broken. When validating a release, run the SPECIFIC flagship/hardest\n  model end-to-end on-device, not just confirm the pipeline completed.\n- **The RACommons/RunAnywhereMLXRuntime checksum-staleness bug above recurred on v0.20.30 AND\n  v0.20.31** — same `mismatch: RACommonsBinary` failure in `publish`, same root cause both\n  times: the version-bump PR ran `sync-versions.sh` and merged/tagged directly, skipping the\n  **mandatory** \"dispatch a release candidate off the unmerged branch, sync checksums from\n  its real build, THEN merge\" sequence that the **`sdk-release` skill's step 2-3 already\n  document in detail** (including this exact failure signature). Documenting the lesson in\n  prose here was not enough to stop it recurring twice more — **if you are about to run\n  `scripts/release/sync-versions.sh` for ANY reason, including as one step of a larger\n  cross-repo release cascade, invoke the `sdk-release` skill and follow it from step 1**,\n  rather than reconstructing the sequence from memory or treating a \"small\" version bump as\n  exempt. v0.20.31 additionally caught `RunAnywhereMLXRuntime` in the Flutter podspec\n  independently stale in the SAME commit — `RAC_CHECKSUM_SKIP` exempts it from `publish`'s\n  check, so it would have shipped broken to Flutter consumers with no CI signal at all; check\n  every row of the checksum-location table, not only the one the CI error names.\n- **QHexRT's `qhx_runtime_device()` (arch/soc lookup, needed to pick the `v75`/`v79`/`v81`\n  manifest directory in `session_open()`) shared its live-device cost with the standard QNN\n  graph path**, so a `host_only` model with ZERO QNN graphs — the Bonsai/Maple ternary decoder\n  (`qwen3.8-27b-1bit-npu`) — paid for a live QNN HTP device anyway, which then contended with\n  the decoder's own direct FastRPC session for the same Hexagon cDSP and made every generation\n  fail with a bare `HostOpFailed`. This shipped in v0.20.30 (the SAME release that fixed the\n  `ADSP_LIBRARY_PATH` bug for the same model) and was found only by running that SPECIFIC model\n  on real Windows-ARM64 hardware after the \"fix\" — reinforcing the point above: a green\n  `HostOpFailed`-free run of a DIFFERENT QHexRT model proves nothing about this one. Two red\n  herrings were chased and ruled out with live device tracing before the real cause: neither\n  `ADSP_LIBRARY_PATH` (confirmed correctly set) nor a QAIRT-version mismatch between the\n  shipped host DLLs and the on-device skel (confirmed present, but fixing it alone did not fix\n  the failure) was the actual cause. Fixed in `neurun` v0.20.31 (`Backend::profile()` now uses\n  a separate, device-handle-free `deviceGetPlatformInfo` query instead of sharing\n  `ensure_device()` with `device()`) + this repo's `qhexrt_session.cpp` (#810, a complementary\n  fix removing a second, redundant eager device query used only for a log line).\n\n---\n\n## CI/CD workflows (`.github/workflows/`)\n\n| Workflow | Trigger | Purpose |\n|----------|---------|---------|\n| `pr-build.yml` | PR to main, push to main/feat branch | Parallel native builds (macOS/Linux/iOS/Android) + per-SDK typecheck + centralization/coherence gates |\n| `release.yml` | Tag `v*.*.*` or manual | Full artifact build matrix, SDK packaging, consumer validation, draft Release |\n| `auto-tag.yml` | PR merged to main with `release:*` label | Verifies the reviewed semver bump, pushes that exact git tag |\n| `idl-drift-check.yml` | Changes to `idl/` or generated files | Regenerates protos, verifies schema lock + untracked generated trees |\n| `legacy-files-blocklist.yml` | All PRs/pushes | Prevents specific deleted files from being re-introduced |\n| `secret-scan.yml` | PRs and pushes to main | Incremental gitleaks scan on diff range |\n| `check-no-pii-logging.yml` | PRs/pushes to main/master/feat-branch | Guards against logging signed URLs alongside active-download destination paths |\n\n---\n\n## Platform requirements\n\n| Platform | Min version | Build tool | Key versions |\n|----------|------------|------------|--------------|\n| iOS / macOS | 17.5 / 14.5 | Xcode 26+ | Swift 6.2 |\n| Kotlin SDK | Android API 24 | AGP 9.2.1 / Gradle 9.5.0 | Kotlin 2.4.0, NDK 27.3.13750724 |\n| Flutter | 3.44.6 | Melos / AGP 9.0.1 / Gradle 9.1.0 | Dart 3.12.2+, NDK 28.2.13676358 |\n| React Native | 0.85.3 (min 0.83.1) | Yarn Berry 3.6.1 | NitroModules, Hermes |\n| Web | Chrome 86+ | Vite | Emscripten 6.0.2, Node 24 LTS |\n| C++ core | N/A | CMake 3.24+ | C++20, Ninja |\n\n---\n\n## Non-obvious configuration details\n\n- **NDK pin**: `core/VERSIONS::NDK_VERSION` (27.3.13750724) is the single source of truth,\n  mirrored into `bindings/kotlin/gradle.properties`. NDK 27 (r27d) gives 16 KB\n  page-alignment required by Android 15+ — NDK 25.x's 4 KB-aligned `libc++_shared.so` /\n  `libomp.so` trips Android 16's page-size enforcement. Mirror this pin whenever bumping\n  it; Flutter/RN Android build files carry their own fallback literals that can drift.\n- **`useLocalNatives`** (Kotlin `gradle.properties`, similarly named flags elsewhere):\n  `true` builds native libs from source locally; CI and most non-local runs set it\n  `false` to download prebuilt `.so`/`.xcframework`/`.wasm` from GitHub Releases instead.\n- **Web SDK** has several hard-won WASM/browser runtime workarounds (VLM worker crash\n  recovery, a Qwen2-VL WebGPU NaN bug, cross-origin isolation requirements) — see\n  [`docs/reference/web-runtime-gotchas.md`](docs/reference/web-runtime-gotchas.md).\n- **Cloud/Linux dev VM** (e.g. Cursor Cloud): what's buildable there and the\n  clang/NDK/KVM gotchas are in\n  [`docs/reference/cursor-cloud-environment.md`](docs/reference/cursor-cloud-environment.md).\n\n---\n\n## Pre-commit hooks\n\n```bash\npre-commit run --all-files                     # everything\npre-commit run ios-sdk-swiftlint --all-files   # SwiftLint only\n```\n\nConfigured hooks: gitleaks (secrets), trailing-whitespace, end-of-file-fixer, check-yaml,\ncheck-added-large-files (1000 KB max), check-merge-conflict, object file detection,\nSwiftLint (SDK + example app), periphery (unused Swift code detection).\n\n---\n\n## Where to look for current in-flight work\n\n`thoughts/shared/plans/` holds active/completed execution plans (including any in-flight\nSDK reorg or migration briefs — e.g. the Electron reorganization plan); `thoughts/shared/issues/`\nholds tracked bug write-ups. Check these directories directly rather than trusting a\nstatus summary written into this file — both change too often for a static snapshot here\nto stay accurate.\n",".claude/skills/telemetry-e2e/SKILL.md":"---\nname: telemetry-e2e\ndescription: Prove that what an SDK binding EMITS on the telemetry wire is correct and that it actually arrives — vocabulary compliance, presence/absence semantics, device-id stability, and a received request body / stored row as evidence. Use when touching telemetry_json.cpp, telemetry_manager.cpp, rac_telemetry_vocabulary.h, any binding's platform-adapter telemetry bridge, or when asked to verify a telemetry field isn't silently dropping.\n---\n\n# Telemetry Wire Correctness (SDK side)\n\nThis is the SDK-side counterpart to the backend's `telemetry-e2e` skill (private\n`Runanywhere-monorepo` repo — proves event-to-stored-row on the server side). This one\nproves the other half: that commons *serializes* a call correctly and that the bytes\n*leaving* a binding are the bytes a real backend accepts. It does not cover per-starter-app\ninference smoke tests — see `sdk-test-starters` for that.\n\n## 0. The evidence bar\n\nCompiling is not evidence. A green `telemetry_extraction_tests` run is not evidence that a\nreal binding's real call site reaches the wire correctly — it only proves the *serializer*\nhandles a hand-built payload correctly. In order of increasing strength:\n\n1. **Unit test green** (`ctest -R telemetry_extraction_tests`) — proves the JSON builder's\n   presence/vocabulary rules are still correct in isolation. Necessary, not sufficient.\n2. **Received request body** — capture the actual JSON `rcli` (or a real binding) put on\n   the wire and read it: right modality endpoint, right keys, no vocabulary values that\n   got through that shouldn't have, measured zeros kept as `0` not `null`.\n3. **Stored row matches, field-for-field** — the backend's own ingest response\n   (`events_received`/`events_stored`/`events_skipped`) or a direct DB read shows the row\n   landed with the values you actually sent, not just a 2xx. This is the only tier that\n   also proves the vocabulary/schema on both sides still agree.\n\nA PR description that says \"sent telemetry, no errors\" without quoting a request body or a\nstored-row count is not evidence of anything except that a socket didn't error out.\n\n## 1. The emit path\n\nEvery binding funnels through the same two C ABI entry points in\n`core/src/infrastructure/telemetry/telemetry_manager.cpp`:\n\n- `rac_telemetry_manager_track` — flat-struct payload (`rac_telemetry_payload_t`), used by\n  `rcli`'s `telemetry emit`/`telemetry blast` and by hand-built events.\n- `rac_telemetry_manager_track_proto` — the real path every language binding actually uses:\n  a binding builds a protobuf `SDKEvent` (`sdk_events.pb.h`) at the real call site (e.g. an\n  LLM `generate()` completing), and `proto_event_type_string()` + the per-domain extraction\n  blocks in `telemetry_manager.cpp` turn it into the same flat payload. `framework_proto_to_string()`\n  / `framework_to_string()` / `clean_framework()` do the backend-name normalization here —\n  this is where a raw enum like `INFERENCE_FRAMEWORK_LLAMA_CPP` becomes the wire string `\"llamacpp\"`.\n- `stamp_live_device_state()` (same file) then overwrites the SDK-origin/device-state fields\n  (`sdk_binding`, `platform`, `battery_state`, memory, core count) from manager-owned state,\n  not from whatever the caller passed — a binding cannot inject a platform or binding value.\n- The payload is handed to `rac_telemetry_manager_payload_to_json()` in\n  `core/src/infrastructure/telemetry/telemetry_json.cpp`, which does field-by-field\n  serialization (see §2/§3 for the two contracts enforced there).\n- `send_batch_json()` batches by modality and POSTs to `/api/v2/sdk/telemetry/{modality}`\n  through whichever `rac_http_transport_ops_t` the binding registered (URLSession/OkHttp/\n  `emscripten_fetch`/curl for `rcli` — see the root `AGENTS.md`'s \"HTTP is platform-provided\"\n  rule). There is no `libcurl` fallback baked into commons itself.\n\nIf a field is going missing on the wire, the fix is almost always in\n`telemetry_json.cpp` (a field never wired into the serializer) or in the call site's proto\npopulation (a field never set on the `SDKEvent`) — not in a binding's bridge code, which\njust forwards bytes.\n\n## 2. The closed vocabularies — and the silent-drop rule\n\n`core/include/rac/infrastructure/telemetry/rac_telemetry_vocabulary.h` is a **generated\nfile** (see §5) holding six closed tables, each with a `RAC_TELEMETRY_<NAME>_COUNT` and a\n`RAC_TELEMETRY_<NAME>_VALUES[]` array:\n\n| Table | Count | Wire field | Values |\n|---|---|---|---|\n| `RAC_TELEMETRY_EVENT_TYPE` | 106 | `event_type` | namespaced strings, e.g. `llm.generation.completed`, `auth.succeeded`, `stt.transcription.completed` |\n| `RAC_TELEMETRY_BACKEND` | 22 | `backend` / `framework` | `onnx, sherpa, llamacpp, foundation_models, system_tts, fluid_audio, coreml, whisperkit_coreml, mlx, qhexrt, neurt, tflite, executorch, mediapipe, mlc, pico_llm, piper_tts, swift_transformers, cloud, builtin, none, unknown` |\n| `RAC_TELEMETRY_MODEL_SOURCE` | 3 | `model_source` | `catalog, builtin, user` |\n| `RAC_TELEMETRY_PLATFORM` | 9 | `platform` | `ios, android, macos, windows, linux, web, tvos, watchos, visionos` |\n| `RAC_TELEMETRY_SDK_BINDING` | 9 | `sdk_binding` | `swift, kotlin, flutter, react-native, web, electron, python, cli, cpp` |\n| `RAC_TELEMETRY_BATTERY_STATE` | 4 | `battery_state` | `charging, full, unplugged, unknown` |\n\n**The rule, verbatim from `add_vocabulary_string()` in `telemetry_json.cpp`:** if a value is\nnon-null but not present in its table, the field is dropped to `null` and the *rest of the\nevent still ships*. This is by design (a bad `backend` string must not cost the whole\nobservation) but it means:\n\n- The backend never sees the bad value, never 422s, never quarantines anything — there is\n  no error path anywhere for this. A field just isn't there.\n- The only client-side signal is a `RAC_LOG_WARNING(\"Telemetry\", \"Dropping %s: '%s' is not\n  in the published vocabulary\", ...)` log line — easy to miss unless you're specifically\n  watching debug logs.\n- `platform`, `sdk_binding`, and `battery_state` are stamped by the manager itself (§1), not\n  supplied by a binding's call site, so in practice only `backend`/`framework` (from the\n  engine/model layer) and `event_type` (only reachable via the proto path, checked at CI\n  time — see §5) are realistic places a new value can silently disappear.\n- `test_telemetry_extraction.cpp`'s vocabulary block (`\"vocab-1\"`/`\"vocab-2\"` cases) is the\n  concrete regression guard: it sends `framework = \"LlamaCpp\"` (a real spelling that used to\n  reach production) and asserts the string never appears on the wire while `model_id` still\n  does. If you touch `add_vocabulary_string()` or add a new vocabulary-gated field, extend\n  that test rather than trusting a manual check.\n\nHistorical motivation (from the header's own comment): before this table existed, stored\nproduction rows had four spellings of `llama.cpp`, three of ONNX, and binding names like\n`\"react-native\"` recorded as a `platform` — this file exists specifically to make that class\nof drift fail in this repo instead of being discovered in a database nobody was watching.\n\n## 3. Presence vs. absence: the \"measured zero\" contract\n\nBefore the fix documented in `telemetry_json.cpp`'s header, extractors read proto3 scalar\nfields directly — which report `0` for a field that was never set — and the old serializer\n(`add_int`/`add_double`) *skipped* zero values to keep payloads small. Result: a genuinely\nmeasured `output_tokens = 0` (an empty completion) and an unmeasured `tokens_per_second`\nboth arrived on the wire as absent, indistinguishable from each other.\n\nThe fix is `has_x`-gated serialization. Two helper families in `telemetry_json.cpp`:\n\n- `add_int_or_null(key, value, is_valid)` / `add_double_or_null(key, value, is_valid)` —\n  emit the real value (including `0`) when `is_valid` is true, else the JSON literal `null`.\n  Non-finite doubles (`NaN`/`Inf`) always degrade to `null` regardless of `is_valid`, because\n  JSON has no such literal and one bad value must not corrupt the whole batch.\n- The `is_valid` argument is a real `has_x`/sentinel check per field, not a blanket flag —\n  e.g. `payload->has_processing_time_ms != RAC_FALSE`, `payload->total_memory > 0`,\n  `payload->battery_level >= 0`.\n\n**What to check to confirm this stayed fixed** (this is exactly what\n`test_telemetry_extraction.cpp`'s presence block does — see §4): send an LLM completion\nevent with `output_tokens = 0` and `input_tokens = 0` explicitly set, and\n`tokens_per_second`/`context_length` left unset. Assert the wire body contains\n`\"output_tokens\":0` and `\"input_tokens\":0` as literal numbers, and `\"tokens_per_second\":null`\n/ `\"context_length\":null` — not all four as `null`, and not all four as `0`. If a future\nrefactor reverts to plain `add_int`/`add_double` for any of these fields, that regression\ntest is the thing that catches it; grep `telemetry_json.cpp` for `add_int(` / `add_double(`\n(the non-`_or_null`/non-`_always` forms) if you suspect a field regressed — those two still\nsilently skip zero by design and must never be used for a field where `0` is meaningful.\n\n## 4. Build, test, and what each telemetry test asserts (macOS)\n\n```bash\ncmake --preset macos-debug\ncmake --build build/macos-debug\nctest --preset macos-debug --output-on-failure -R 'telemetry|device_identity'\n```\n\nOr the non-preset form (`core/AGENTS.md`'s documented path):\n\n```bash\ncmake -B build -DRAC_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Debug\ncmake --build build\nctest --test-dir build --output-on-failure\n```\n\n`telemetry_extraction_tests` and `device_identity_tests` are registered in\n`core/tests/CMakeLists.txt`; `rcli_telemetry_live_tests` is registered separately in\n`rcli/tests/CMakeLists.txt`. All three are hand-rolled assertion runners — `CHECK()` in\nsome files, `ASSERT_EQ`/`ASSERT_TRUE` (from `core/tests/test_common.h`) in others — never\nGoogleTest:\n\n| ctest name | Binary | What it asserts |\n|---|---|---|\n| `telemetry_extraction_tests` | `test_telemetry_extraction` | Full proto→JSON pipeline per modality (LLM token counts/`tokens_per_second`, STT NaN-confidence never leaks as the string `\"nan\"`, TTS `characters_per_second`, embeddings `total_tokens`/`batch_size`/`embedding_dimension`, LoRA failure-path fields, RAG retrieval counts, VLM image/timing fields, live device-state stamping); the presence/measured-zero contract (§3); the closed-vocabulary drop rule (§2); and an HTTP-retry backoff (a failed batch is re-sent only after >5s, never before). Requires `RAC_HAVE_PROTOBUF` — the test binary itself no-ops with exit 0 if commons wasn't built with protobuf support. |\n| `device_identity_tests` | `test_device_identity` | Every branch of `rac_device_get_or_create_persistent_id()`'s resolution chain (§6) against a mocked platform adapter — cache hit, vendor-ID fallback, fresh-UUID generation, and that a `secure_set` failure doesn't silently drop the ID. |\n| `rcli_telemetry_live_tests` | `test_rcli_telemetry_live` | **Opt-in, always registered but a no-op by default.** Runs only with `--live` *and* `RUNANYWHERE_BASE_URL`/`RUNANYWHERE_API_KEY` set; otherwise prints \"skip\" and exits 0 — a green ctest run for this target proves nothing by itself. When live, it runs `rcli::bootstrap()` (real API-key → device-register → JWT handshake), sends one real event per modality (LLM, embeddings, RAG, VLM, LoRA-failure) through the real HTTP transport, and fails on any non-2xx response — this is the test that would catch a strict-schema `422 extra_forbidden` from field drift against the real V2 endpoints. |\n\nRun it live (§7 has the full local-backend flow):\n\n```bash\nRUNANYWHERE_BASE_URL=https://<backend-origin> RUNANYWHERE_API_KEY=<key> \\\n  ./build/macos-debug/rcli/tests/test_rcli_telemetry_live --live\n```\n\n`test_platform_bridge` (same `CMakeLists.txt`, adjacent target) is the wider companion\ncheck — \"every value a binding supplies must reach the wire\" — covering the platform half\nof the same two flat C structs (`rac_client_info_t`/`rac_device_registration_info_t`) for\nevery SDK at once; run it alongside if you're touching client-info fields rather than\ntelemetry event fields specifically.\n\n## 5. Regenerating the vocabulary header, and the drift gate\n\n`rac_telemetry_vocabulary.h` (§2) is generated from `idl/http/sdk-openapi.json` — the\ndevice-facing OpenAPI subset copied in from the private backend repo. **How that file gets\nupdated on the backend side, and how it crosses into this repo, is the monorepo's\n`openapi-contract` skill's job — read that by name in the other repo rather than\nre-deriving the steps here.** From inside this repo, once `idl/http/sdk-openapi.json` is\ncurrent:\n\n```bash\npython3 idl/codegen/generate_telemetry_vocabulary.py          # regenerate the header\npython3 idl/codegen/generate_telemetry_vocabulary.py --check  # CI drift check; exit 1 if stale\n```\n\n`.github/workflows/telemetry-vocabulary-drift.yml` runs three checks on every push to\n`main`/`development` and on any PR touching `idl/http/**`, the codegen script, or the\ntelemetry source/header dirs:\n\n1. **Generated header is current** — the `--check` command above.\n2. **Every event type the core can emit is in the vocabulary** — a Python check that parses\n   `proto_event_type_string()`'s body out of `telemetry_manager.cpp` via regex, unions in\n   the two prefix-expansion families (`stt.model.*`/`tts.voice.*`/`llm.model.*` and\n   `llm*`/`vlm*` generation suffixes), and diffs against `TelemetryEventType`'s enum in\n   `idl/http/sdk-openapi.json`. Anything emittable but unpublished fails the job.\n3. **Every framework string the core can emit is in the vocabulary** — same technique\n   against `framework_proto_to_string()` and `framework_to_string()` vs. `TelemetryBackend`.\n\n**A documented drift you will hit if you trust the wrong file**: `idl/http/README.md`'s own\n\"What it pins\" table names the schema for the `framework`/`backend` field as\n`TelemetryFramework` — that's stale. The actual generator\n(`idl/codegen/generate_telemetry_vocabulary.py`'s `VOCABULARIES` dict) and the header it\nproduces both say `TelemetryBackend`. Trust the generator and the generated header's own\n`// Published as X` comments over that README table.\n\n## 6. Per-binding device identity: where it's created, where it's persisted\n\nThe resolution chain itself is centralized in commons —\n`core/src/infrastructure/device/device_identity.cpp`\n(`rac_device_get_or_create_persistent_id`, tested by `device_identity_tests`, §4) — and is\nthe same fixed order for every binding:\n\n1. `adapter.secure_get(\"com.runanywhere.sdk.device.uuid\")` — if found and non-empty, use it,\n   full stop.\n2. `adapter.get_vendor_id` (if the platform adapter provides one) — use it, then persist it\n   via `secure_set` so step 1 hits next boot.\n3. Generate a fresh RFC-4122 v4 UUID, persist it via `secure_set`.\n\nWhat differs per binding is **only** the `secure_get`/`secure_set` backing store — an\nunstable backing store (one that doesn't survive a real app restart, or that resolves to a\ndifferent physical location than the read path expects) is exactly the bug class that\ninflates device counts, because step 3 fires again on every \"cache miss\":\n\n| Binding | Backing store | Where |\n|---|---|---|\n| Swift (iOS/macOS) | Keychain, service `com.runanywhere.sdk` | `bindings/swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+PlatformAdapter.swift` (`platformSecureGetCallback`/`platformSecureSetCallback`) |\n| Kotlin (Android) | Android Keystore AES-256-GCM key, ciphertext in a no-backup app file (not `SharedPreferences`) | `bindings/kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/security/AndroidKeychainManager.kt` (`NoBackupCiphertextStore`), wired through `CppBridgePlatformAdapter.PlatformSecureStorage` |\n| Flutter | Native Keychain/Keystore helper via FFI symbols `ra_flutter_secure_storage_{store,retrieve,delete}` | `bindings/flutter/packages/runanywhere/lib/native/dart_bridge_secure_storage.dart`; the device-UUID key constant is duplicated (deliberately, per its own comment) as `_keyDeviceUUID = 'com.runanywhere.sdk.device.uuid'` in `dart_bridge_device.dart` |\n| React Native (Android) | `SecureStorageManager` (native Android side) via `PlatformAdapterBridge.kt`'s `secureGet`/`secureSet` | `bindings/react-native/packages/core/android/src/main/java/com/margelo/nitro/runanywhere/PlatformAdapterBridge.kt` |\n| Web | `localStorage`, key `rac_sdk_plaintext_com.runanywhere.sdk.device.uuid` — **plaintext**, no OS-level encryption equivalent exists in a browser; restricted by convention to non-sensitive IDs | `bindings/web/packages/core/src/runtime/PlatformAdapter.ts` (`registerSecureGet`/`registerSecureSet`); also has a separate `stableVendorId()` fallback under key `rac_sdk_plaintext_vendor_id` |\n| Electron (macOS/Linux) | Flat file under a secure directory, path-traversal-guarded (`fs::path(key)` must not be absolute or contain `..`) | `bindings/electron/native/posix_platform_adapter.cpp` (`posix_secure_get`/`posix_secure_set`) |\n| Electron (Windows) | Windows DPAPI (`CryptProtectData`, current-user scope, no UI) | `bindings/electron/native/win32_platform_adapter.cpp` (`win_secure_get`/`win_secure_set`) |\n\nThe key string itself — `com.runanywhere.sdk.device.uuid` — is intentionally the same\nliteral across Swift, Flutter, and RN (each source comments cross-reference the other two by\nname), so grepping for it is a fast way to confirm a new binding is reading/writing the\n*same* key it thinks it is, not a typo'd sibling.\n\n**How to verify stability across a real restart** (not just \"the code looks right\"):\n\n1. Run the binding once, capture the emitted `device_id` from a real request body (§7) or\n   from `POST /api/v1/devices/register`'s response.\n2. Fully terminate the process (not just background it — on mobile this matters, a\n   backgrounded app can keep its in-memory device-id cache alive without ever touching\n   the read path again).\n3. Relaunch and repeat step 1.\n4. The `device_id` must be byte-identical. If it changed, the break is almost always one of:\n   the backing store not surviving process death (e.g. an in-memory-only stub used during\n   development), a key-name mismatch between the write and read paths, or (Android\n   specifically) `Context.deleteSharedPreferences` being invoked on the wrong file — see\n   `AndroidKeychainManager.kt`'s constructor, which deletes a hardcoded *legacy* prefs name\n   on every construction; if that name were ever widened to match the real store, every\n   Android launch would look like a fresh device.\n\nTwo-in-one launches on the *same physical device* (e.g. two emulator instances, or a\nsame-device reinstall vs. a real fresh install) are a different, real-world failure mode\nfrom a code bug — don't conflate an intentional reinstall's fresh ID with instability.\n\n## 7. Point a local build at a local backend and prove an event landed\n\n`rcli` is the fastest way to exercise the full wire path without a mobile/browser\ntoolchain — it links `rac_commons` directly and speaks real HTTP via curl.\n\n```bash\ncmake --preset rcli-macos-release   # or rcli-linux-release on Linux\ncmake --build build/rcli-macos-release --target rcli -j \"$(sysctl -n hw.logicalcpu)\"\n```\n\n**Keyless (development environment) — no API key, works against any backend that allows\nanonymous V2 telemetry:**\n\n```bash\n./build/rcli-macos-release/rcli/rcli --environment development \\\n  --base-url http://127.0.0.1:8000 \\\n  telemetry blast --processing-ms 42.5 --session-id my-check \\\n  --input-tokens 128 --output-tokens 256\n```\n\nThis is exactly the shape `scripts/ci/oss_keyless_telemetry_blast.sh` runs in CI against the\npublic staging backend — `--environment development --base-url <origin> telemetry blast\n...` with no `--api-key`. `telemetry blast` sends one representative event per modality (all\n12) and prints a `MODALITY | RESULT | STATUS | RECEIVED | STORED | SKIPPED` table parsed\ndirectly from the backend's own batch response — **the `STORED` column is authoritative\nserver-side confirmation**, not just an HTTP 2xx. `run_telemetry_blast()`'s own exit-code\nlogic (`rcli/src/commands/cmd_telemetry.cpp`) does fold `stored >= count` into its overall\npass/fail, so a genuine storage shortfall on any modality does make the process exit\nnon-zero, not just a transport error — but the exit code is one bit for all 12 modalities\ncombined, so a non-zero exit still requires reading the table to know *which* modality\nfailed and *why* (a `network error` status is a transport problem; an `HTTP 200` row with\n`STORED` under `RECEIVED` is a row-level rejection, e.g. a vocabulary value the backend\ndidn't recognize — a real bug to chase, not the same failure class at all).\n\n**Authenticated (production shape) — for exercising the login handshake too:**\n\n```bash\n./build/rcli-macos-release/rcli/rcli --environment production \\\n  --base-url https://<backend-origin> --api-key <key> \\\n  auth login\n./build/rcli-macos-release/rcli/rcli --environment production \\\n  --base-url https://<backend-origin> --api-key <key> \\\n  telemetry emit --modality llm --count 3 --input-tokens 128 --output-tokens 256\n```\n\n`auth login` only has a path in `production` — `development`'s keyless mode has no login\nstep by design (see `rcli/src/commands/cmd_auth.cpp`'s own subcommand help text).\n\n**To inspect the actual request body**, not just the summary table, add `--json` for\nmachine-readable CLI output and `-v` for debug logging (the debug log includes the\nserialized JSON per batch before it goes on the wire) — or point `--base-url` at a throwaway\nlocal capture endpoint (any script that logs the raw POST body) before pointing it at a real\nbackend, to sanity-check field shape offline first.\n\n**If you have the private backend repo checked out locally too**, running it yourself\n(`cd backend && uv run uvicorn main:app --reload`, per that repo's own README) and pointing\n`--base-url` at `http://127.0.0.1:8000` gets you the strongest possible evidence: query the\nSQLite/Postgres row directly after a blast and diff it field-for-field against what you sent\n— the monorepo's own `telemetry-e2e` skill (its integration suite, hermetic pytest fixtures,\nand `test_telemetry_conformance.py`'s vocabulary/presence/identity assertions against stored\nrows) is the authoritative reference for that side; don't re-implement its DB assertions\nhere, just point at it.\n\n## Evidence bar, restated\n\n- Compiling `rcli` or a binding: **not evidence**.\n- `ctest -R telemetry_extraction_tests` green: **not evidence of a real call site** — only\n  of the serializer in isolation.\n- `rcli telemetry blast` exiting 0: real signal (its exit code does fold in the backend's\n  `stored >= count` check, §7) but still worth pairing with the per-modality\n  `STORED`/`SKIPPED` table in your report — the exit code can't tell you *which* modality\n  or *why* a shortfall happened.\n- A captured request body showing the right endpoint, the right keys, correct\n  presence/absence per §3, and no off-vocabulary values per §2 — **evidence of correct\n  emission**.\n- That request body's values matching a stored row (via the backend's own\n  `events_stored` count at minimum, a direct DB read at best) — **evidence of correct\n  end-to-end delivery**, the only tier worth reporting as \"telemetry works.\"\n",".claude/skills/sdk-publish/SKILL.md":"---\nname: sdk-publish\ndescription: Manually publish an already-tagged, already-released (non-draft GitHub Release) RunAnywhere SDK version to every package registry (npm, Maven Central, pub.dev, Swift SPM dist repo) plus macOS rcli notarization, with heavy verification that nothing fake/stubbed/broken ships.\n---\n\n# SDK Publish\n\nRepo: `~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks` (verify with `pwd`/`git remote get-url origin` — do not assume this path is still correct, checkouts move). Version source of truth: `core/VERSION`. Throughout, `$VERSION` means the bare semver being published (e.g. `0.20.24`), never prefixed with `v` except in git tags / GitHub Release names.\n\n## THIS PROCESS IS INTENTIONALLY MANUAL\n\n**`release.yml` never publishes anywhere public.** It only builds artifacts, packages them, and creates a draft/published **GitHub Release**. It does not call `npm publish`, does not upload to Maven Central, does not call `flutter pub publish`, and does not push the Swift dist repo. Verify this is still true before trusting it — greps below should all return **zero** matches (grep the whole file, not a snippet):\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks\ngrep -inE \"npm publish|publishReleasePublication|publishAllPublicationsToMavenCentral|flutter pub publish\" .github/workflows/release.yml\n```\n\nThis is a **deliberate safety gate**, not a gap. Every step below is a human (or an agent acting on a human's explicit go-ahead) pushing a button that CI intentionally never pushes. Do not propose wiring any of this into CI as a \"fix\" — that would remove the gate.\n\n## Precondition check (always run first)\n\nRefuse to proceed if the release is a draft or the asset set looks incomplete.\n\n```bash\ngh release view v$VERSION --json isDraft,tagName --jq '{isDraft, tagName}'\n# isDraft MUST be false.\n\ngh release view v$VERSION --json assets --jq '.assets[].name' | sort\n```\n\nCross-reference the listing against the `sdk-release` skill's step 8 asset-count expectations (`~/.claude/skills/sdk-release/SKILL.md`). As a second, independent check, also diff against the **previous** tagged release's asset list — same count, same name patterns, only the version token differs:\n\n```bash\ngh release view v<PREVIOUS_VERSION> --json assets --jq '.assets[].name' | sed \"s/<PREVIOUS_VERSION>/$VERSION/\" | sort >/tmp/expected.txt\ngh release view v$VERSION --json assets --jq '.assets[].name' | sort >/tmp/actual.txt\ndiff /tmp/expected.txt /tmp/actual.txt\n```\n\nAs of the last verified run (**v0.20.25, 65 assets** — up from 55 at v0.20.24 because the 5 public Electron tarballs and their sidecars joined the release; see §6), the full set is: `MANIFEST.txt`; per-platform `RACommons-{android-arm64-v8a,android-armeabi-v7a,android-x86_64,ios,linux-x86_64,web,windows-x64}-v$VERSION.{zip,tar.gz}` + `.sha256`; iOS backend zips `RABackend{LLAMACPP,MLX,NeuRT,ONNX,Sherpa}-ios-v$VERSION.zip` + `.sha256`; `RunAnywhereMLX{Metal,Resources,Runtime}-ios-v$VERSION.zip` + `.sha256`; `rcli-{linux-x86_64,macos-arm64,windows-x86_64}-v$VERSION.tar.gz`/`.zip` + `.sha256`; the 8 npm tarballs `runanywhere-{core,llamacpp,mlx,onnx,proto-ts,web,web-llamacpp,web-onnx}-$VERSION.tgz` + `.sha256`; the **5 Electron npm tarballs** `runanywhere-electron{,-llamacpp,-onnx,-sherpa,-neurt}-$VERSION.tgz` + `.sha256` (new as of v0.20.25); and `runanywhere-kotlin-maven-v$VERSION.zip` + `.sha256`. There is **no** Flutter tarball in the release (Flutter packages are staged from the native archives above, not from a dedicated release asset) and **no** `.dmg` asset in the baseline set (the macOS rcli tarball is what ships; see the notarization section for when a `.dmg` does get produced). If a `qhexrt`-named asset appears anywhere in this list, stop — that would mean `release.yml`'s own manifest assertion should have hard-failed and something is very wrong; do not publish anything from that release.\n\n## 0. MANDATORY GATE — verify the ENTIRE release before pushing anything to any registry\n\n**Run this once, in full, before the first registry push. Do not publish a single package until it passes.** Per-package spot-checks are not a substitute: they are how a bad artifact reaches a registry you cannot un-publish from. Proven end to end on v0.20.25 (2026-08-22), then independently audited and corrected: **34 payloads, 6619 files across 3 nesting levels, 282 binaries, 0 placeholder hits, 38/38 commons binaries carrying the real staging host**. The audit found 4 methodology gaps in the first pass (shallow nesting, missing `MTLB` magic, `.lib` excluded from the positive check, and a set-intersection that could pass vacuously) — every one is called out inline below, because each produced a *confident but under-covered* result.\n\n```bash\nVERSION=0.20.25                      # no leading v\nREPO=RunanywhereAI/runanywhere-sdks\nW=/tmp/relverify-$VERSION\nrm -rf \"$W\" && mkdir -p \"$W/assets\" \"$W/qhexrt\" \"$W/x\" \"$W/nested\"\n\n# (1) EVERY release asset — no -p filter, get the whole set.\ngh release download \"v$VERSION\" --repo \"$REPO\" -D \"$W/assets\"\n\n# (2) The PRIVATE QHexRT artifact is NOT a release asset — pull it from the run.\n#     Find the run that produced the release, then:\ngh run download <release-run-id> --repo \"$REPO\" --name sdk-electron-qhexrt-private --dir \"$W/qhexrt\"\n```\n\n### (3) Checksums — `cd` into the directory first\n\nSidecars contain a **bare basename**, so `shasum -c` fails with a misleading \"FAILED open or read\" if run from anywhere else. This will make every artifact look broken when nothing is wrong:\n\n```bash\ncd \"$W/assets\"\nok=0; bad=0\nfor s in *.sha256; do\n  if shasum -a 256 -c \"$s\" >/dev/null 2>&1; then ok=$((ok+1)); else echo \"MISMATCH: $s\"; bad=$((bad+1)); fi\ndone\necho \"verified=$ok mismatched=$bad\"      # bad MUST be 0\n(cd \"$W/qhexrt\" && shasum -a 256 -c ./*.sha256)\n```\n\n### (4) Extract everything — INCLUDING nested archives\n\n**The gotcha that matters:** `runanywhere-kotlin-maven-v*.zip` contains `.aar` files, and the `.aar`s are what hold the Android `.so` binaries. A one-level extraction reports \"0 binaries\" for the Maven bundle and every subsequent scan passes **vacuously**.\n\n**Recurse until the tree is dry — TWO levels are required here, not one.** Each `.aar` also contains a `classes.jar`, holding ~2027 `.class` files that are doubly unreachable: inside an unopened jar *and* excluded by the `.class` suffix rule most magic-byte scanners use. Level 2 adds **2033 files** (0 placeholder hits on v0.20.25); level 3 is empty. Loop until a pass finds no new archives rather than hardcoding a depth.\n\n```bash\ncd \"$W/assets\"\nfor f in *; do\n  case \"$f\" in *.sha256|MANIFEST.txt) continue;; esac\n  d=\"$W/x/$f\"; mkdir -p \"$d\"\n  case \"$f\" in\n    *.zip)              unzip -qq -o \"$f\" -d \"$d\" || echo \"EXTRACT FAILED: $f\" ;;\n    *.tar.gz|*.tgz)     tar -xzf \"$f\" -C \"$d\"     || echo \"EXTRACT FAILED: $f\" ;;\n    *) echo \"UNHANDLED TYPE: $f\" ;;\n  esac\ndone\nd=\"$W/x/$(basename \"$W\"/qhexrt/*.tgz)\"; mkdir -p \"$d\"; tar -xzf \"$W\"/qhexrt/*.tgz -C \"$d\"\n\n# nested pass\ncd \"$W/x\"\nfind . -type f \\( -name '*.aar' -o -name '*.jar' -o -name '*.zip' -o -name '*.tgz' -o -name '*.tar.gz' \\) \\\n  | sed 's|^\\./||' | while IFS= read -r a; do\n    dd=\"$W/nested/$(printf '%s' \"$a\" | tr '/' '_')\"; mkdir -p \"$dd\"\n    case \"$a\" in *.aar|*.jar|*.zip) unzip -qq -o \"$a\" -d \"$dd\";; *) tar -xzf \"$a\" -C \"$dd\";; esac\n  done\n```\n\n**Prove extraction actually happened** — an empty dir makes \"0 hits\" meaningless:\n\n```bash\nfor d in \"$W\"/x/*/; do\n  n=$(find \"$d\" -type f | wc -l)\n  [ \"$n\" -lt 2 ] && echo \"SUSPICIOUSLY EMPTY: $d ($n files)\"\ndone\n```\n\n### (5) Placeholder scan — EVERY file, not just binaries\n\nThe staging URL can also live in JS/JSON/TS config, so scan by content across all file types, and additionally scan the raw compressed streams in case a member never got extracted:\n\n```bash\nfind \"$W/x\" \"$W/nested\" -type f | wc -l                                     # total files covered\ngrep -rla \"YOUR_STAGING_BASE_URL\" \"$W/x\" \"$W/nested\" | tee /tmp/ph.txt | wc -l   # MUST be 0\nfor f in \"$W\"/assets/*.tgz \"$W\"/assets/*.tar.gz \"$W\"/qhexrt/*.tgz; do\n  gzip -dc \"$f\" 2>/dev/null | grep -qa \"YOUR_STAGING_BASE_URL\" && echo \"RAW HIT: $f\"\ndone\n```\n\nFind binaries by **magic bytes, not extension** (an unsuffixed framework binary like `RACommons.framework/RACommons` has no extension and a name-based `find` misses it). Signatures to cover:\n\n| Magic | Kind | v0.20.25 count |\n|---|---|---|\n| `7f454c46` | ELF | 177 |\n| `!<arch>` (`213c617263683e0a`) | static `.a` **and `.lib`** | 41 |\n| `MZ` | PE | 29 |\n| `cffaedfe` / `cefaedfe` | Mach-O | 19 |\n| `00 61 73 6d` | wasm | 10 |\n| **`4d544c42` (`MTLB`)** | **Metal library** — easy to miss | 5 |\n| `cafebabe` / `bebafeca` | fat Mach-O (exclude `.jar`/`.class`) | 1 |\n\n**Total: 282 binaries.** An earlier pass reported 277 because `MTLB` was absent from the signature list — `.metallib` files were invisible to the binary scan (they were still covered by the all-file `grep` in step 5, so the conclusion held, but the count was wrong). If your total comes in materially under ~282 for a comparable release, your detection is leaking.\n\n### (6) POSITIVE staging-URL check — absence of the placeholder is NOT sufficient\n\nA build with an **empty** URL baked in passes a placeholder-only check. `check_plugin_natives.py` only tests absence, so add the positive assertion yourself.\n\nTwo traps that produced false confidence on the v0.20.25 pass, both worth avoiding:\n\n- **Include `.lib`.** A `MUST_HAVE` regex of `rac_commons\\.(dylib|so|dll|a)$` silently skips\n  `RACommons-windows-x64/x64/rac_commons.lib` — an **89 MB** binary that then gets no positive\n  check at all. Match `(dylib|so|dll|a|lib)` plus `\\.wasm$`. Correct coverage is **38** binaries,\n  not 37.\n- **Assert per binary, not by set-intersection.** Intersecting \"non-public origins across all\n  files\" yields more than one origin here (`hf.co` *and* the staging host), so a binary carrying\n  only `hf.co` would pass **vacuously**. Check each binary individually for the staging host.\n\n```bash\n# v0.20.25 baseline: 38 commons/wasm/lib binaries; 0 missing the staging host.\n```\n\nNever print the URL — it is a repo secret. Report a truncated SHA-256 fingerprint, or just assert presence/absence counts.\n\n### (7) Routability gate over the whole tree\n\n```bash\npython3 scripts/validation/gates/check_plugin_natives.py \"$W/x\" \"$W/nested\" > /tmp/gate.txt 2>&1\ngrep -cE '^  ok ' /tmp/gate.txt; grep -E '^  FAIL' /tmp/gate.txt\n```\n\nv0.20.25 baseline: **77 ok, 1 FAIL**. The single expected FAIL is\n`RACommons-linux-x86_64/lib/librunanywhere_sherpa.so` — see the known-exception note below. **Any other FAIL blocks the publish.**\n\n### (8) Known exception — Linux sherpa is a non-routable shell (PRE-EXISTING, not a regression)\n\n`RACommons-linux-x86_64-v*.tar.gz` ships **no** `libsherpa-onnx-c-api.so` at all and its `librac_backend_sherpa.so` is a ~156 KB shell (~3 sherpa strings, vs ~31 MB / ~192 for a healthy one on Electron macOS) — the engine is not linked in, so the carrier cannot reference `g_sherpa_{stt,tts,vad}_ops` and **STT/TTS/VAD cannot work from that bundle**. Android's carriers pass on all 3 ABIs, so this is Linux-only.\n\n**Compare the CARRIER, not the backend.** The gate FAILs on `librunanywhere_sherpa.so` (the carrier); citing `librac_backend_sherpa.so` as your evidence is comparing a *different file* and is not proof. The carrier is byte-identical across releases, which is the real proof:\n\n```bash\ngh release download \"v<PREV>\" --repo \"$REPO\" -p 'RACommons-linux-x86_64-v<PREV>.tar.gz' -D /tmp/prev\ntar -xzf /tmp/prev/*.tar.gz -C /tmp/prev\nshasum -a 256 /tmp/prev/lib/librunanywhere_sherpa.so \"$W\"/x/RACommons-linux-*/lib/librunanywhere_sherpa.so\n# v0.20.24 and v0.20.25 both: size=15712  sha256=28ef16015b61da86...\n\n# strongest check — run the gate on the PREVIOUS bundle and confirm the identical FAIL:\npython3 scripts/validation/gates/check_plugin_natives.py /tmp/prev\n```\n\nIf it ever regresses *further* (e.g. Android or macOS sherpa also goes hollow), stop and treat it as a blocking defect.\n\n### (9) Account for every payload explicitly\n\nPrint a per-artifact table (checksum verdict, binary count, placeholder hits) and confirm the row count equals the payload count. Artifacts that legitimately contain **no** native binaries, and why — do not let these pass silently unexamined:\n\n| Artifact | Why it has no binaries |\n|---|---|\n| `RunAnywhereMLXResources-ios-*.zip` | 25 third-party license/notice text files |\n| `runanywhere-kotlin-maven-v*.zip` | binaries live in the **nested** `.aar`s (step 4) |\n| `runanywhere-proto-ts-*.tgz` | pure TS/JS (69 `.ts` + 69 `.js`) |\n\nEverything else must have a non-zero, plausible binary count. A platform suddenly shipping fewer binaries than the previous release is a regression signal worth chasing before publishing.\n\n### (10) Blind spot to be aware of — Windows x64 has NO routability coverage\n\n`RACommons-windows-x64-v*.zip` ships **only** a static `rac_commons.lib` (~89 MB) plus 152 `.h` / 11 `.hpp` headers — **zero backend DLLs**, where Linux/Android/Web each ship 4–5 backend carriers. That is consistent with a static-embedding bundle (consumers link the engines in themselves), but the consequence matters: **no gate can detect a missing or hollow Windows backend**, because there is no carrier to inspect. `check_plugin_natives.py` therefore reports nothing at all for this artifact rather than reporting health.\n\nWindows x64 is also *advisory* in `release.yml` (its absence does not fail the release). So a Windows regression can pass every automated check. If Windows correctness matters for a given release, verify it by actually running something on the real Windows box (see §Electron for how to reach it) — do not infer it from a green pipeline. Worth a deliberate confirmation with the repo owner that the DLL-free shape is intentional.\n\n---\n\n## General verification discipline (every artifact, every registry)\n\nApply all three checks below to **every** file you're about to publish, before AND after the publish call. Never trust a copy cached locally earlier in the session — re-download.\n\n**(a) Download + verify the sha256 sidecar yourself:**\n\n```bash\ngh release download v$VERSION -p '<file>' -p '<file>.sha256' -D /tmp/sdk-publish-$VERSION\ncd /tmp/sdk-publish-$VERSION && shasum -a 256 -c '<file>.sha256'\n```\n\n**(b) Inspect the actual file listing inside every tarball/AAR/zip** — never assume from the filename alone:\n\n```bash\nnpm pack <tgz-or-package-dir> --dry-run     # npm packages: prints the exact file list it would publish\nunzip -l  <file.zip>                        # AARs, Maven repo zip, native archives\ntar -tzf  <file.tar.gz>                     # rcli, RACommons tarballs\n```\n\nSanity-check what comes back:\n- No suspiciously-small or empty binaries where a real native library (`.so`, `.dylib`, `.a`, `.dll`) should be — a stub or a zero-byte placeholder means the build silently failed upstream.\n- No leftover `workspace:*` or `file:` dependency specs in any `package.json` inside the tarball — those only resolve inside this monorepo and break for a real external installer.\n- Version strings inside every manifest (`package.json` `version`, AAR `pom.xml` `<version>`, `pubspec.yaml` `version:`) **exactly** match `$VERSION`.\n- **Hard rule, no exceptions:** grep the full file listing for `qhexrt` (any case) and for QNN binary names (`libQnnHtp`, `QnnHtp*.dll`, `rac_commons.dll` bundled with QHexRT, `runanywhere_qhexrt`). If any hit appears in a package meant to be public, stop and do not publish that artifact.\n\n**(c) Confirm baked-in runtime config** (e.g. a staging-vs-prod backend base URL compiled into `rac_commons` at build time) points at the real intended endpoint. This is designed to fail closed if misconfigured — a build that completed and produces working requests against prod is itself decent evidence — but if you have any doubt, `strings <binary> | grep -i \"api\\.\\|staging\\.\"` and eyeball it.\n\n## 1. npm — 8 packages, order matters\n\nPackages: 4 Web (`@runanywhere/proto-ts`, `@runanywhere/web`, `@runanywhere/web-llamacpp`, `@runanywhere/web-onnx`) + 4 React Native (`@runanywhere/core`, `@runanywhere/llamacpp`, `@runanywhere/mlx`, `@runanywhere/onnx`). Never `@runanywhere/qhexrt` (RN) — it exists in `bindings/react-native/packages/qhexrt/` but is excluded from the public npm train by the same policy as Maven/pub.dev QHexRT.\n\n**Publish `@runanywhere/proto-ts` FIRST.** `@runanywhere/web-onnx` deliberately does not bundle its own `proto-ts` copy and needs it resolvable from the live registry for real consumers; publish `proto-ts` before it or a fresh `npm install @runanywhere/web-onnx` fails to resolve.\n\n```bash\ngh release download v$VERSION -p 'runanywhere-proto-ts-*.tgz*' -p 'runanywhere-web-*.tgz*' \\\n  -p 'runanywhere-core-*.tgz*' -p 'runanywhere-llamacpp-*.tgz*' -p 'runanywhere-mlx-*.tgz*' -p 'runanywhere-onnx-*.tgz*' \\\n  -D /tmp/sdk-publish-$VERSION\ncd /tmp/sdk-publish-$VERSION\nfor f in *.sha256; do shasum -a 256 -c \"$f\"; done\n\nnpm publish runanywhere-proto-ts-$VERSION.tgz --access public   # FIRST, always\nnpm publish runanywhere-web-$VERSION.tgz --access public\nnpm publish runanywhere-web-llamacpp-$VERSION.tgz --access public\nnpm publish runanywhere-web-onnx-$VERSION.tgz --access public\nnpm publish runanywhere-core-$VERSION.tgz --access public\nnpm publish runanywhere-llamacpp-$VERSION.tgz --access public\nnpm publish runanywhere-mlx-$VERSION.tgz --access public\nnpm publish runanywhere-onnx-$VERSION.tgz --access public\n```\n\n`npm whoami` should already show a logged-in account; if it errors, stop and tell the human rather than trying to log in non-interactively.\n\n**GOTCHA — large tarballs can silently die mid-upload.** `@runanywhere/mlx` (~50MB) and `@runanywhere/web-onnx` (unpacks to 300+MB) can exceed a short default shell/tool timeout mid-upload and get killed with no clean error. Use an explicit long timeout (5 minutes / 300000ms) on each publish call.\n\n**GOTCHA — verify via the registry API, not the npm CLI.** `npm view` can lag the CLI's own cache by several seconds right after a fresh publish and falsely report \"not found\". After **every** publish:\n\n```bash\ncurl -s \"https://registry.npmjs.org/@runanywhere/<pkg>\" | python3 -c \"import json,sys;print(json.load(sys.stdin)['dist-tags'])\"\n```\nConfirm `latest` equals `$VERSION`.\n\n## 2. Maven Central — 3 AARs only\n\n`io.github.sanchitmonga22:{runanywhere-sdk,runanywhere-llamacpp,runanywhere-onnx}`. **Never `runanywhere-qhexrt-android`** — it was published through 0.20.19 under an earlier, since-reversed policy and is now permanently excluded; `release.yml`'s manifest assertion hard-fails if it reappears in the public asset set.\n\nFull step-by-step lives in `bindings/kotlin/docs/KOTLIN_MAVEN_CENTRAL_PUBLISHING.md`, section **\"Option A: Publish with released native archives\"** (line ~108 as of this writing — the doc has been corrected mid-release before, so re-read it fresh rather than trusting memory of its contents). Key points, condensed:\n\n1. Download + verify the 3 `RACommons-android-{arm64-v8a,armeabi-v7a,x86_64}-v$VERSION.zip` from the release (sha256 sidecars, per the general discipline above).\n2. Stage: `bash bindings/kotlin/scripts/package-sdk.sh --mode local --natives-from <dir-with-the-3-zips>`. This also produces a local Maven-repo zip — diff its size against `runanywhere-kotlin-maven-v$VERSION.zip` already attached to the GitHub Release as a cheap reproducibility check.\n3. Before uploading, confirm the OSSRH compatibility service has no dangling open repository for this namespace:\n   ```bash\n   CENTRAL_BEARER=\"$(printf '%s:%s' \"$MAVEN_CENTRAL_USERNAME\" \"$MAVEN_CENTRAL_PASSWORD\" | base64 | tr -d '\\r\\n')\"\n   curl -s -H \"Authorization: Bearer $CENTRAL_BEARER\" \\\n     'https://ossrh-staging-api.central.sonatype.com/manual/search/repositories?ip=client&profile_id=io.github.sanchitmonga22'\n   ```\n   Resolve any stale repository per the doc's troubleshooting table before continuing.\n4. Source `MAVEN_CENTRAL_USERNAME`/`MAVEN_CENTRAL_PASSWORD` from `~/.gradle/gradle.properties` (`mavenCentral.username`/`mavenCentral.password` — confirmed present on this machine; never print the values) and run, from `bindings/kotlin/`:\n   ```bash\n   ./gradlew clean \\\n     :publishReleasePublicationToMavenCentralRepository \\\n     :modules:runanywhere-core-llamacpp:publishReleasePublicationToMavenCentralRepository \\\n     :modules:runanywhere-core-onnx:publishReleasePublicationToMavenCentralRepository \\\n     -Prunanywhere.useLocalNatives=true -x buildLocalJniLibs --no-daemon\n   ```\n   GPG signing (`signing.gnupg.keyName`/`signing.gnupg.passphrase` in the same properties file, key id `CC377A9928C7BB18` — confirmed importable via `gpg --list-secret-keys --keyid-format LONG` on this machine) is read from the same properties file automatically; you do not pass it separately.\n5. Transfer to Central Portal:\n   ```bash\n   curl --fail --request POST -H \"Authorization: Bearer $CENTRAL_BEARER\" \\\n     'https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/io.github.sanchitmonga22?publishing_type=automatic'\n   ```\n   Then find the resulting `portal_deployment_id` — expect **exactly one** fresh repository; if the count isn't 1, stop and investigate rather than guessing which one is yours (see the doc for the exact `jq` filter).\n6. Poll `https://central.sonatype.com/api/v1/publisher/status?id=<id>` (POST, same bearer) until `deploymentState` is `PUBLISHED`. Automatic deployments normally go `PENDING -> VALIDATING -> PUBLISHING -> PUBLISHED` within a few minutes. **If it lands on `VALIDATED` instead of proceeding, that means Sonatype is waiting for a manual click in the Central Portal UI** — stop and tell the human; do not loop forever assuming it resolves itself. On `FAILED`, print `.errors` and stop.\n7. Verify propagation (allow 10-30 minutes for CDN propagation even after `PUBLISHED`):\n   ```bash\n   for a in runanywhere-sdk runanywhere-llamacpp runanywhere-onnx; do\n     echo \"$a: $(curl -s -o /dev/null -w '%{http_code}' \"https://repo1.maven.org/maven2/io/github/sanchitmonga22/$a/$VERSION/$a-$VERSION.pom\")\"\n   done\n   ```\n   Expect `200` for all three.\n\n## 3. pub.dev (Flutter)\n\n**Verify the current package set fresh — do not trust a hardcoded count.** As of this writing the real, non-QHexRT Flutter package set is **4**, not 3: `runanywhere`, `runanywhere_llamacpp`, `runanywhere_onnx`, and `runanywhere_mlx` (mlx was added after some earlier documentation/memory of \"3 packages\" was written — that assumption is stale; `runanywhere_mlx` is a real, already-published-on-pub.dev package, not new/WIP). Enumerate it yourself instead of trusting this list verbatim next time:\n```bash\nfind bindings/flutter/packages -maxdepth 1 -type d -not -iname '*qhexrt*' -not -path '*/packages'\n```\n`runanywhere_qhexrt` exists in that directory too — never publish it (same exclusion policy as Maven).\n\nStage the released iOS xcframeworks + Android `.so` into each package with the Flutter equivalent of `package-sdk.sh` (melos-based monorepo, confirmed at `bindings/flutter/scripts/package-sdk.sh`):\n\n```bash\nbash bindings/flutter/scripts/package-sdk.sh --mode local --natives-from <dir-with-xcframeworks-and-android-.so>\n```\n\nThis script already runs `flutter pub publish --dry-run` internally per package as part of its own validation — but run it again yourself, per package, from each package directory, and **read the output for real, not just the exit code**:\n\n```bash\ncd bindings/flutter/packages/runanywhere && flutter pub publish --dry-run\ncd ../runanywhere_llamacpp && flutter pub publish --dry-run\ncd ../runanywhere_onnx && flutter pub publish --dry-run\ncd ../runanywhere_mlx && flutter pub publish --dry-run\n```\n\nExpect zero warnings except a benign version-gap hint. Only then run the real publish, same order (core first, since the others depend on it):\n\n```bash\ncd bindings/flutter/packages/runanywhere && flutter pub publish\ncd ../runanywhere_llamacpp && flutter pub publish\ncd ../runanywhere_onnx && flutter pub publish\ncd ../runanywhere_mlx && flutter pub publish\n```\n\n**Credential note:** pub.dev OAuth is normally cached at `~/Library/Application Support/dart/pub-credentials.json` on macOS (confirmed present on this machine). If it's missing, expired, or `flutter pub publish` prompts for an interactive browser login, **STOP and tell the human** — you cannot complete a fresh Google OAuth flow non-interactively, and forcing/guessing here is not appropriate.\n\nVerify afterward against the live API (not a cached page):\n```bash\nfor p in runanywhere runanywhere_llamacpp runanywhere_onnx runanywhere_mlx; do\n  echo \"$p: $(curl -s \"https://pub.dev/api/packages/$p\" | python3 -c \"import json,sys;print(json.load(sys.stdin)['latest']['version'])\")\"\ndone\n```\n\n## 4. Swift SPM distribution repo (`RunanywhereAI/runanywhere-swift`)\n\nA separate, **generated-only** repo mirroring `bindings/swift` (Package.swift + Sources/ + LICENSE + README) so SwiftPM consumers using `from: \"<version>\"` don't clone the ~340MB monorepo.\n\n**GOTCHA:** this machine may have local directories literally named `runanywhere-swift` that are just checkouts of the **monorepo's** remote, not the distribution repo. Verify before trusting any existing local directory by name:\n```bash\ngit -C <candidate-dir> remote get-url origin\n```\nMust show `RunanywhereAI/runanywhere-swift`, not `runanywhere-sdks`. Always clone fresh instead of guessing:\n\n```bash\ngit clone https://github.com/RunanywhereAI/runanywhere-swift.git /tmp/ra-swift\n\nbindings/swift/scripts/sync-dist-repo.sh \\\n  --zips <exact-verified-Apple-archive-dir-from-the-release> --tag /tmp/ra-swift\n\nRUNANYWHERE_SWIFT_DIST_REPO=/tmp/ra-swift \\\n  bash scripts/validation/gates/check_swift_dist_repo_sync.sh\n\ngit -C /tmp/ra-swift push origin main --follow-tags\n```\n\nThe `--zips` dir must contain the verified (sha256-checked) release archives — `RACommons-ios-v$VERSION.zip`, `RABackend{LLAMACPP,MLX,NeuRT,ONNX,Sherpa}-ios-v$VERSION.zip`, `RunAnywhereMLX{Metal,Resources,Runtime}-ios-v$VERSION.zip` — downloaded and checked exactly per the general verification discipline, not re-used from an earlier, untrusted local copy.\n\n**The tag on `runanywhere-swift` is bare semver, no `v` prefix** (SwiftPM's `from:` needs that) — do not tag it `v$VERSION` by mistake; `sync-dist-repo.sh --tag` handles this correctly, but double check the pushed tag with `git -C /tmp/ra-swift tag --points-at HEAD` before walking away.\n\nThis is enforced, not just documented: once `v$VERSION` is tagged on `runanywhere-sdks`, `check_swift_dist_repo_sync.sh` fails every future PR on the monorepo until `runanywhere-swift` carries the matching bare-semver tag — so don't let this lag behind the other registries.\n\n**GATE GAP — `check_swift_dist_repo_sync.sh` does NOT catch dependency drift, only checksum/version-string drift.** `runanywhere-swift`'s `Package.swift` is manually maintained end to end — `sync-dist-repo.sh` only ever touches `sdkVersion` and the binaryTarget checksums, never the `dependencies:` block (mlx-swift, mlx-swift-lm, mlx-audio-swift, swift-crypto, etc.). If root `runanywhere-sdks/Package.swift` bumps one of those (e.g. a new fork tag), a human has to remember to mirror it into the dist repo by hand, and nothing fails CI if they forget. This exact drift shipped once (2026-08-17): the dist repo pointed `mlx-swift-lm` at unforked `ml-explore/mlx-swift-lm` instead of the `RunanywhereAI` fork, silently breaking every iOS/macOS MLX consumer of that tag until caught by an actual build failure ~40 minutes later. Before trusting a freshly-cut dist-repo tag: diff every `.package(url:` line between root `Package.swift` and the dist repo's `Package.swift` (`grep -n '\\.package(url:' Package.swift` on both sides), and actually build a real product that exercises the dependency you're worried about (e.g. `cd /tmp/ra-swift && swift build --product RunAnywhereMLX`) — don't rely on the sync gate alone. If you catch drift on a tag that's only minutes old with no evidence of external consumption, fixing it in place and force-moving the tag (`git tag -f $VERSION && git push origin $VERSION --force`) is the correct call, not a shortcut — a known-broken immutable tag helps no one.\n\n**If you force-move a tag, SwiftPM/Xcode consumers cache the OLD commit in several places — clearing just `Package.resolved` is not enough.** Verifying your own fix (or having a downstream consumer verify it) can silently keep failing even after the fix is correctly pushed, because: (1) any consumer repo's own tracked `*.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved` pins the exact old resolution and `xcodebuild -resolvePackageDependencies` respects it rather than re-resolving; (2) `~/Library/Caches/org.swift.swiftpm/repositories/<pkg>-<hash>/` is a global shared clone cache keyed by repo, oblivious to a moved tag; (3) `~/Library/org.swift.swiftpm/security/fingerprints/<pkg>-<hash>.json` records the fingerprint SwiftPM saw for that tag and can silently keep using it after a mismatch instead of failing loud. To actually re-resolve after moving a tag: delete the consumer's tracked `Package.resolved`, `rm -rf .build .swiftpm` and any `~/Library/Developer/Xcode/DerivedData/<Scheme>-*`, AND clear the matching entries under both global `org.swift.swiftpm` cache directories for every affected package identity — then resolve fresh.\n\n## 5. macOS rcli notarization\n\nCI (`native_rcli_macos` in `release.yml`) usually already produces a notarized, stapled artifact when Developer ID + notary secrets are configured in repo secrets. **Check the release assets first** before assuming you need to notarize anything yourself locally:\n\n```bash\ngh release view v$VERSION --json assets --jq '.assets[].name' | grep -iE 'rcli-macos|\\.dmg'\n```\nBaseline expectation is `rcli-macos-arm64-v$VERSION.tar.gz` (+ `.sha256`); a `.dmg` only appears when `RCLI_MACOS_FULL_RELEASE=1` was set for that build (see below). If a plain tarball is already present and its contents pass the general verification discipline, there is usually nothing further to do here.\n\n**If you do need to notarize locally**, everything lives in `rcli/scripts/package-rcli.sh` (read its header comment — it documents its own contract in detail):\n\n- One-time setup, check before redoing: `xcrun notarytool history --keychain-profile runanywhere-notary` (confirmed configured on this machine — a fresh submission history returns without error). If missing: `xcrun notarytool store-credentials \"runanywhere-notary\" --apple-id <email> --team-id <team> --password <app-specific-password>`.\n- Codesign identity: `security find-identity -v -p codesigning | grep \"Developer ID Application\"` — confirmed present on this machine as `\"Developer ID Application: RunAnywhere, Inc (<TEAM_ID>)\"`. Grep for it fresh rather than hardcoding the team-id suffix, it can rotate.\n- Invocation contract (env vars, from the script header): `RCLI_CODESIGN_IDENTITY`, `RCLI_MACOS_NOTARIZE=1`, `RCLI_MACOS_FULL_RELEASE=1`, `RCLI_MACOS_SWIFT_BIN_DIR=<output of build-mlx-cli.sh>`. `RCLI_MACOS_FULL_RELEASE=1` is what additionally stages `mlx.metallib` and SwiftPM resource bundles and is what causes a notarized, stapled `.dmg` to be emitted alongside the Homebrew-style tarball. `RCLI_CODESIGN_KEYCHAIN` / `RCLI_NOTARYTOOL_KEYCHAIN` matter only if the identity/profile live in a non-default keychain.\n- Call shape: `rcli/scripts/package-rcli.sh <build-dir> macos-arm64` with the env vars above exported.\n\n## 6. Electron — build AND packaging are now in `release.yml`; publish is still manual\n\n**Reversing the earlier \"deliberately separate\" note**: as of `fix/electron-cicd-staging-url-npu-ane` (2026-08-21), the repo owner asked for Electron to produce ready-to-publish artifacts through the real release train like every other SDK, so `release.yml` now has `native_electron` (invokes `.github/workflows/electron-native-package.yml` as a reusable `workflow_call`) and `sdk_electron` (downloads its `electron-packaged-tarballs` artifact, runs `scripts/release/prepublish_check.py` on every tarball, checksums them, and splits public vs. QHexRT-private). `publish` hard-gates on `sdk_electron` succeeding. `@runanywhere/electron`, `-llamacpp`, `-onnx`, `-sherpa`, `-neurt` land as real, `.sha256`-sidecarred GitHub Release assets, same as the Web/RN npm tarballs. `@runanywhere/electron-qhexrt` still never becomes a public Release asset (policy below) — it's produced and packaged every release, but only reachable as the `sdk-electron-qhexrt-private` workflow artifact on that run, for a human to `npm publish` by hand. Electron is **not** WIP on a branch anymore: the old root `AGENTS.md` \"Active issues\" section describing `smonga/electron_upgrade` as in-progress is gone entirely as of this checkout — `bindings/electron/` is a fully merged, real SDK on `main`. Re-check that section is still absent before trusting this note.\n\n`.github/workflows/electron-native-package.yml` itself is unchanged in what it builds: every backend for real (llamacpp/onnx/sherpa/neurt on their real platforms, qhexrt on a self-hosted Windows ARM64 runner) with the real `STAGING_BASE_URL` baked in, gated by `check_plugin_natives.py` (which fails closed on the staging-URL placeholder — the exact bug that shipped through `0.20.24`). What's new is that it's also invocable via `workflow_call`, and that `native_win_x64_and_package` now tolerates a failed/skipped QHexRT lane (self-hosted, single machine) rather than losing the other five real packages to one runner's outage. **`release.yml` itself still does not `npm publish` anything** — it only produces artifacts (Release assets for the public five, a workflow artifact for QHexRT) — same \"build automated, publish manual\" shape as every other platform. See `thoughts/shared/plans/electron_cicd_staging_url_and_npu_ane_pipeline.md` for the full history.\n\n**PROVEN END TO END on v0.20.25 (2026-08-22)** — the first release where this actually ran. Final state: 65 release assets, all 5 public Electron tarballs present with `.sha256` sidecars, zero `qhexrt`-named assets. Verified by downloading the *published* tarballs and running the repo's own gate against them:\n\n```bash\ngh release download v<version> --repo RunanywhereAI/runanywhere-sdks -p 'runanywhere-electron*.tgz' -D /tmp/verify\npython3 scripts/validation/gates/check_plugin_natives.py --expect-version <version> \\\n    $(for t in /tmp/verify/*.tgz; do printf -- '--tarball %s ' \"$t\"; done)\n# => All 20 check(s) passed: plugin natives are routable and no staging-URL placeholder was found.\n```\n\nThat single command asserts all three things you actually care about, per binary: the real `STAGING_BASE_URL` is baked in (no `YOUR_STAGING_BASE_URL`), `rac_commons` embeds the right version, and every backend is genuinely **routable** (`g_llamacpp_ops`, `g_sherpa_stt_ops`, `g_neurt_llm_ops`, `g_onnx_embeddings_ops`, …) rather than a same-sized shell. **Run it before every Electron `npm publish`** — it is the cheapest defense against exactly the class of defect that shipped `electron-sherpa` 0.20.17's NULL-vtable carrier. Expected shipped layout: entry package = `darwin-arm64` + `win32-x64` + `win32-arm64`; llamacpp/onnx/sherpa = `darwin-arm64` + `win32-x64`; neurt = `darwin-arm64` only; qhexrt = `win32-arm64` only.\n\nWhere to get the QHexRT tarball for its manual publish (it is deliberately NOT a release asset):\n\n```bash\ngh run download <release-run-id> --repo RunanywhereAI/runanywhere-sdks \\\n    --name sdk-electron-qhexrt-private --dir /tmp/qhexrt\n```\n\nIf asked to actually publish an Electron package (still a real, separate, manual step — this skill's general discipline in §0/§General verification still applies in full):\n- Involve the Windows box for real Windows-native verification before publishing anything Windows-targeted. The team keeps a **Windows ARM64 machine reachable over SSH**; its hostname, user, key, and mesh-VPN node name are operator-specific and deliberately not recorded in this repo — read the Windows/ARM64 host entry out of the operator's `~/.ssh/config` and use that alias. If there is no such entry, the box is not configured for you: say so and ask, rather than guessing at a hostname. Check reachability **first** by asking the mesh-VPN client whether the node is up (e.g. `tailscale status`, matching on the host entry's `HostName`) — if it reports `offline, last seen ...`, the box is asleep, which is not a configuration problem: ask the human to wake it, do not debug it as broken. Only then `ssh -o ConnectTimeout=8 \"$WIN_BOX\" \"whoami\"`. That same machine is **also** registered as a GitHub Actions self-hosted runner (labels `[self-hosted, Windows, ARM64, qhexrt]`) running as a Windows service — a queued `native_win_arm64_qhexrt` job on it means the machine is busy; check `gh api repos/RunanywhereAI/runanywhere-sdks/actions/runners` for the matching runner's `busy`/`status` before assuming it's free for manual SSH work, and vice versa.\n- **`@runanywhere/electron-qhexrt` being public on npm is a CONFIRMED, settled policy decision** (repo owner, 2026-08-20) — it contains real Hexagon NPU binaries (`libQnnHtpV81Skel.so`, `QnnHtp*.dll`, `rac_commons.dll`, `runanywhere_qhexrt.dll`) on purpose. The QHexRT-stays-private policy enforced everywhere else in this skill is specifically about the **GitHub-Release-side** asset set (Android etc.), a different channel — not a contradiction. Stop treating this as an unresolved item to flag; only re-raise it if you find evidence the policy changed.\n\n## 7. Final sanity pass\n\nAfter every registry is done, re-view each package's public listing/API and compare against the previous version — a big unexplained drop in file count or package size is a red flag to investigate before calling the release fully published:\n\n```bash\n# npm\nfor p in proto-ts web web-llamacpp web-onnx core llamacpp mlx onnx; do\n  npm pack @runanywhere/$p@$VERSION --dry-run 2>&1 | tail -5\ndone\n\n# Maven — check module metadata / POM sizes are non-trivial, not near-empty\nfor a in runanywhere-sdk runanywhere-llamacpp runanywhere-onnx; do\n  curl -s -o /dev/null -w \"%{size_download} bytes  $a\\n\" \\\n    \"https://repo1.maven.org/maven2/io/github/sanchitmonga22/$a/$VERSION/$a-$VERSION.aar\"\ndone\n\n# pub.dev\nfor p in runanywhere runanywhere_llamacpp runanywhere_onnx runanywhere_mlx; do\n  curl -s \"https://pub.dev/api/packages/$p\" | python3 -c \"import json,sys;d=json.load(sys.stdin);print('$p', d['latest']['version'])\"\ndone\n```\n\nOnly after this pass — every registry shows `$VERSION`, every artifact's contents check out, and nothing regressed in size/file-count versus the previous release — is the release fully published. Do not skip this step just because each individual publish call reported success; a successful upload is not the same claim as \"the right bytes are now live.\"\n","core/AGENTS.md":"# core - AI Agent Protocol & Instructions\n\nPath: `core/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/core/AGENTS.md)","core/CLAUDE.md":"# core - Claude Agent Guidelines & System Prompt\n\nPath: `core/CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/core/CLAUDE.md)","engines/AGENTS.md":"# engines - AI Agent Protocol & Instructions\n\nPath: `engines/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/engines/AGENTS.md)","engines/CLAUDE.md":"# engines - Claude Agent Guidelines & System Prompt\n\nPath: `engines/CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/engines/CLAUDE.md)","idl/AGENTS.md":"# idl - AI Agent Protocol & Instructions\n\nPath: `idl/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/idl/AGENTS.md)","idl/CLAUDE.md":"# idl - Claude Agent Guidelines & System Prompt\n\nPath: `idl/CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/idl/CLAUDE.md)","runtimes/AGENTS.md":"# runtimes - AI Agent Protocol & Instructions\n\nPath: `runtimes/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/runtimes/AGENTS.md)","runtimes/CLAUDE.md":"# runtimes - Claude Agent Guidelines & System Prompt\n\nPath: `runtimes/CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/runtimes/CLAUDE.md)","scripts/AGENTS.md":"# scripts - AI Agent Protocol & Instructions\n\nPath: `scripts/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/scripts/AGENTS.md)","scripts/CLAUDE.md":"# scripts - Claude Agent Guidelines & System Prompt\n\nPath: `scripts/CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/scripts/CLAUDE.md)",".claude/commands/address_PR_comments.md":"# Command: /address_pr_comments\n\nPath: `.claude/commands/address_PR_comments.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/.claude/commands/address_PR_comments.md)",".claude/commands/copy_over_logic.md":"# Command: /copy_over_logic\n\nPath: `.claude/commands/copy_over_logic.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/.claude/commands/copy_over_logic.md)",".claude/commands/run_and_fix_lints.md":"# Command: /run_and_fix_lints\n\nPath: `.claude/commands/run_and_fix_lints.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/.claude/commands/run_and_fix_lints.md)","bindings/electron/packages/llamacpp/README.md":"# llamacpp Documentation\n\nPath: `bindings/electron/packages/llamacpp/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/llamacpp/README.md)","bindings/electron/packages/llamacpp/package-lock.json":"# Llamacpp Skill\n\nPath: `bindings/electron/packages/llamacpp/package-lock.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/llamacpp/package-lock.json)","bindings/electron/packages/llamacpp/package.json":"# Llamacpp Skill\n\nPath: `bindings/electron/packages/llamacpp/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/llamacpp/package.json)","bindings/electron/packages/llamacpp/tsconfig.json":"# Llamacpp Skill\n\nPath: `bindings/electron/packages/llamacpp/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/llamacpp/tsconfig.json)","bindings/electron/packages/neurt/README.md":"# neurt Documentation\n\nPath: `bindings/electron/packages/neurt/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/neurt/README.md)","bindings/electron/packages/neurt/package-lock.json":"# Neurt Skill\n\nPath: `bindings/electron/packages/neurt/package-lock.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/neurt/package-lock.json)","bindings/electron/packages/neurt/package.json":"# Neurt Skill\n\nPath: `bindings/electron/packages/neurt/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/neurt/package.json)","bindings/electron/packages/neurt/tsconfig.json":"# Neurt Skill\n\nPath: `bindings/electron/packages/neurt/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/neurt/tsconfig.json)","bindings/electron/packages/onnx/README.md":"# onnx Documentation\n\nPath: `bindings/electron/packages/onnx/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/onnx/README.md)","bindings/electron/packages/onnx/package-lock.json":"# Onnx Skill\n\nPath: `bindings/electron/packages/onnx/package-lock.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/onnx/package-lock.json)","bindings/electron/packages/onnx/package.json":"# Onnx Skill\n\nPath: `bindings/electron/packages/onnx/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/onnx/package.json)","bindings/electron/packages/onnx/tsconfig.json":"# Onnx Skill\n\nPath: `bindings/electron/packages/onnx/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/onnx/tsconfig.json)","bindings/electron/packages/qhexrt/README.md":"# qhexrt Documentation\n\nPath: `bindings/electron/packages/qhexrt/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/qhexrt/README.md)","bindings/electron/packages/qhexrt/package-lock.json":"# Qhexrt Skill\n\nPath: `bindings/electron/packages/qhexrt/package-lock.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/qhexrt/package-lock.json)","bindings/electron/packages/qhexrt/package.json":"# Qhexrt Skill\n\nPath: `bindings/electron/packages/qhexrt/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/qhexrt/package.json)","bindings/electron/packages/qhexrt/tsconfig.json":"# Qhexrt Skill\n\nPath: `bindings/electron/packages/qhexrt/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/qhexrt/tsconfig.json)","bindings/electron/packages/sherpa/README.md":"# sherpa Documentation\n\nPath: `bindings/electron/packages/sherpa/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/sherpa/README.md)","bindings/electron/packages/sherpa/package-lock.json":"# Sherpa Skill\n\nPath: `bindings/electron/packages/sherpa/package-lock.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/sherpa/package-lock.json)","bindings/electron/packages/sherpa/package.json":"# Sherpa Skill\n\nPath: `bindings/electron/packages/sherpa/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/sherpa/package.json)","bindings/electron/packages/sherpa/tsconfig.json":"# Sherpa Skill\n\nPath: `bindings/electron/packages/sherpa/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/sherpa/tsconfig.json)","bindings/flutter/packages/runanywhere/CHANGELOG.md":"# Runanywhere Skill\n\nPath: `bindings/flutter/packages/runanywhere/CHANGELOG.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere/CHANGELOG.md)","bindings/flutter/packages/runanywhere/README.md":"# runanywhere Documentation\n\nPath: `bindings/flutter/packages/runanywhere/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere/README.md)","bindings/flutter/packages/runanywhere/pubspec.yaml":"# Runanywhere Skill\n\nPath: `bindings/flutter/packages/runanywhere/pubspec.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere/pubspec.yaml)","bindings/flutter/packages/runanywhere_llamacpp/CHANGELOG.md":"# Runanywhere_llamacpp Skill\n\nPath: `bindings/flutter/packages/runanywhere_llamacpp/CHANGELOG.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_llamacpp/CHANGELOG.md)","bindings/flutter/packages/runanywhere_llamacpp/README.md":"# runanywhere_llamacpp Documentation\n\nPath: `bindings/flutter/packages/runanywhere_llamacpp/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_llamacpp/README.md)","bindings/flutter/packages/runanywhere_llamacpp/pubspec.yaml":"# Runanywhere_llamacpp Skill\n\nPath: `bindings/flutter/packages/runanywhere_llamacpp/pubspec.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_llamacpp/pubspec.yaml)","bindings/flutter/packages/runanywhere_mlx/CHANGELOG.md":"# Runanywhere_mlx Skill\n\nPath: `bindings/flutter/packages/runanywhere_mlx/CHANGELOG.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_mlx/CHANGELOG.md)","bindings/flutter/packages/runanywhere_mlx/README.md":"# runanywhere_mlx Documentation\n\nPath: `bindings/flutter/packages/runanywhere_mlx/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_mlx/README.md)","bindings/flutter/packages/runanywhere_mlx/pubspec.yaml":"# Runanywhere_mlx Skill\n\nPath: `bindings/flutter/packages/runanywhere_mlx/pubspec.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_mlx/pubspec.yaml)","bindings/flutter/packages/runanywhere_onnx/CHANGELOG.md":"# Runanywhere_onnx Skill\n\nPath: `bindings/flutter/packages/runanywhere_onnx/CHANGELOG.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_onnx/CHANGELOG.md)","bindings/flutter/packages/runanywhere_onnx/README.md":"# runanywhere_onnx Documentation\n\nPath: `bindings/flutter/packages/runanywhere_onnx/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_onnx/README.md)","bindings/flutter/packages/runanywhere_onnx/pubspec.yaml":"# Runanywhere_onnx Skill\n\nPath: `bindings/flutter/packages/runanywhere_onnx/pubspec.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_onnx/pubspec.yaml)","bindings/flutter/packages/runanywhere_qhexrt/CHANGELOG.md":"# Runanywhere_qhexrt Skill\n\nPath: `bindings/flutter/packages/runanywhere_qhexrt/CHANGELOG.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_qhexrt/CHANGELOG.md)","bindings/flutter/packages/runanywhere_qhexrt/README.md":"# runanywhere_qhexrt Documentation\n\nPath: `bindings/flutter/packages/runanywhere_qhexrt/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_qhexrt/README.md)","bindings/flutter/packages/runanywhere_qhexrt/pubspec.yaml":"# Runanywhere_qhexrt Skill\n\nPath: `bindings/flutter/packages/runanywhere_qhexrt/pubspec.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_qhexrt/pubspec.yaml)","bindings/react-native/packages/core/README.md":"# core Documentation\n\nPath: `bindings/react-native/packages/core/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/core/README.md)","bindings/react-native/packages/core/nitro.json":"# Core Skill\n\nPath: `bindings/react-native/packages/core/nitro.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/core/nitro.json)","bindings/react-native/packages/core/package.json":"# Core Skill\n\nPath: `bindings/react-native/packages/core/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/core/package.json)","bindings/react-native/packages/core/tsconfig.json":"# Core Skill\n\nPath: `bindings/react-native/packages/core/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/core/tsconfig.json)","bindings/react-native/packages/core/tsconfig.test.json":"# Core Skill\n\nPath: `bindings/react-native/packages/core/tsconfig.test.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/core/tsconfig.test.json)","bindings/react-native/packages/llamacpp/README.md":"# llamacpp Documentation\n\nPath: `bindings/react-native/packages/llamacpp/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/llamacpp/README.md)","bindings/react-native/packages/llamacpp/nitro.json":"# Llamacpp Skill\n\nPath: `bindings/react-native/packages/llamacpp/nitro.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/llamacpp/nitro.json)","bindings/react-native/packages/llamacpp/package.json":"# Llamacpp Skill\n\nPath: `bindings/react-native/packages/llamacpp/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/llamacpp/package.json)","bindings/react-native/packages/llamacpp/tsconfig.json":"# Llamacpp Skill\n\nPath: `bindings/react-native/packages/llamacpp/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/llamacpp/tsconfig.json)","bindings/react-native/packages/mlx/README.md":"# mlx Documentation\n\nPath: `bindings/react-native/packages/mlx/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/mlx/README.md)","bindings/react-native/packages/mlx/package.json":"# Mlx Skill\n\nPath: `bindings/react-native/packages/mlx/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/mlx/package.json)","bindings/react-native/packages/mlx/tsconfig.json":"# Mlx Skill\n\nPath: `bindings/react-native/packages/mlx/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/mlx/tsconfig.json)","bindings/react-native/packages/onnx/README.md":"# onnx Documentation\n\nPath: `bindings/react-native/packages/onnx/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/onnx/README.md)","bindings/react-native/packages/onnx/nitro.json":"# Onnx Skill\n\nPath: `bindings/react-native/packages/onnx/nitro.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/onnx/nitro.json)","bindings/react-native/packages/onnx/package.json":"# Onnx Skill\n\nPath: `bindings/react-native/packages/onnx/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/onnx/package.json)","bindings/react-native/packages/onnx/tsconfig.json":"# Onnx Skill\n\nPath: `bindings/react-native/packages/onnx/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/onnx/tsconfig.json)","bindings/react-native/packages/qhexrt/README.md":"# qhexrt Documentation\n\nPath: `bindings/react-native/packages/qhexrt/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/qhexrt/README.md)","bindings/react-native/packages/qhexrt/nitro.json":"# Qhexrt Skill\n\nPath: `bindings/react-native/packages/qhexrt/nitro.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/qhexrt/nitro.json)","bindings/react-native/packages/qhexrt/package.json":"# Qhexrt Skill\n\nPath: `bindings/react-native/packages/qhexrt/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/qhexrt/package.json)","bindings/react-native/packages/qhexrt/tsconfig.json":"# Qhexrt Skill\n\nPath: `bindings/react-native/packages/qhexrt/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/qhexrt/tsconfig.json)","bindings/react-native/packages/qhexrt/tsconfig.test.json":"# Qhexrt Skill\n\nPath: `bindings/react-native/packages/qhexrt/tsconfig.test.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/qhexrt/tsconfig.test.json)","bindings/web/packages/core/README.md":"# core Documentation\n\nPath: `bindings/web/packages/core/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/core/README.md)","bindings/web/packages/core/package.json":"# Core Skill\n\nPath: `bindings/web/packages/core/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/core/package.json)","bindings/web/packages/core/tsconfig.json":"# Core Skill\n\nPath: `bindings/web/packages/core/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/core/tsconfig.json)","bindings/web/packages/core/tsconfig.test.json":"# Core Skill\n\nPath: `bindings/web/packages/core/tsconfig.test.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/core/tsconfig.test.json)","bindings/web/packages/llamacpp/README.md":"# llamacpp Documentation\n\nPath: `bindings/web/packages/llamacpp/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/llamacpp/README.md)","bindings/web/packages/llamacpp/package.json":"# Llamacpp Skill\n\nPath: `bindings/web/packages/llamacpp/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/llamacpp/package.json)","bindings/web/packages/llamacpp/tsconfig.json":"# Llamacpp Skill\n\nPath: `bindings/web/packages/llamacpp/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/llamacpp/tsconfig.json)","bindings/web/packages/onnx/README.md":"# onnx Documentation\n\nPath: `bindings/web/packages/onnx/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/onnx/README.md)","bindings/web/packages/onnx/package.json":"# Onnx Skill\n\nPath: `bindings/web/packages/onnx/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/onnx/package.json)","bindings/web/packages/onnx/tsconfig.json":"# Onnx Skill\n\nPath: `bindings/web/packages/onnx/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/onnx/tsconfig.json)"},"files":{".agents/skills/telemetry-e2e/SKILL.md":"---\nname: telemetry-e2e\ndescription: Prove that what an SDK binding EMITS on the telemetry wire is correct and that it actually arrives — vocabulary compliance, presence/absence semantics, device-id stability, and a received request body / stored row as evidence. Use when touching telemetry_json.cpp, telemetry_manager.cpp, rac_telemetry_vocabulary.h, any binding's platform-adapter telemetry bridge, or when asked to verify a telemetry field isn't silently dropping.\n---\n\n# Telemetry Wire Correctness (SDK side)\n\nThis is the SDK-side counterpart to the backend's `telemetry-e2e` skill (private\n`Runanywhere-monorepo` repo — proves event-to-stored-row on the server side). This one\nproves the other half: that commons *serializes* a call correctly and that the bytes\n*leaving* a binding are the bytes a real backend accepts. It does not cover per-starter-app\ninference smoke tests — see `sdk-test-starters` for that.\n\n## 0. The evidence bar\n\nCompiling is not evidence. A green `telemetry_extraction_tests` run is not evidence that a\nreal binding's real call site reaches the wire correctly — it only proves the *serializer*\nhandles a hand-built payload correctly. In order of increasing strength:\n\n1. **Unit test green** (`ctest -R telemetry_extraction_tests`) — proves the JSON builder's\n   presence/vocabulary rules are still correct in isolation. Necessary, not sufficient.\n2. **Received request body** — capture the actual JSON `rcli` (or a real binding) put on\n   the wire and read it: right modality endpoint, right keys, no vocabulary values that\n   got through that shouldn't have, measured zeros kept as `0` not `null`.\n3. **Stored row matches, field-for-field** — the backend's own ingest response\n   (`events_received`/`events_stored`/`events_skipped`) or a direct DB read shows the row\n   landed with the values you actually sent, not just a 2xx. This is the only tier that\n   also proves the vocabulary/schema on both sides still agree.\n\nA PR description that says \"sent telemetry, no errors\" without quoting a request body or a\nstored-row count is not evidence of anything except that a socket didn't error out.\n\n## 1. The emit path\n\nEvery binding funnels through the same two C ABI entry points in\n`core/src/infrastructure/telemetry/telemetry_manager.cpp`:\n\n- `rac_telemetry_manager_track` — flat-struct payload (`rac_telemetry_payload_t`), used by\n  `rcli`'s `telemetry emit`/`telemetry blast` and by hand-built events.\n- `rac_telemetry_manager_track_proto` — the real path every language binding actually uses:\n  a binding builds a protobuf `SDKEvent` (`sdk_events.pb.h`) at the real call site (e.g. an\n  LLM `generate()` completing), and `proto_event_type_string()` + the per-domain extraction\n  blocks in `telemetry_manager.cpp` turn it into the same flat payload. `framework_proto_to_string()`\n  / `framework_to_string()` / `clean_framework()` do the backend-name normalization here —\n  this is where a raw enum like `INFERENCE_FRAMEWORK_LLAMA_CPP` becomes the wire string `\"llamacpp\"`.\n- `stamp_live_device_state()` (same file) then overwrites the SDK-origin/device-state fields\n  (`sdk_binding`, `platform`, `battery_state`, memory, core count) from manager-owned state,\n  not from whatever the caller passed — a binding cannot inject a platform or binding value.\n- The payload is handed to `rac_telemetry_manager_payload_to_json()` in\n  `core/src/infrastructure/telemetry/telemetry_json.cpp`, which does field-by-field\n  serialization (see §2/§3 for the two contracts enforced there).\n- `send_batch_json()` batches by modality and POSTs to `/api/v2/sdk/telemetry/{modality}`\n  through whichever `rac_http_transport_ops_t` the binding registered (URLSession/OkHttp/\n  `emscripten_fetch`/curl for `rcli` — see the root `AGENTS.md`'s \"HTTP is platform-provided\"\n  rule). There is no `libcurl` fallback baked into commons itself.\n\nIf a field is going missing on the wire, the fix is almost always in\n`telemetry_json.cpp` (a field never wired into the serializer) or in the call site's proto\npopulation (a field never set on the `SDKEvent`) — not in a binding's bridge code, which\njust forwards bytes.\n\n## 2. The closed vocabularies — and the silent-drop rule\n\n`core/include/rac/infrastructure/telemetry/rac_telemetry_vocabulary.h` is a **generated\nfile** (see §5) holding six closed tables, each with a `RAC_TELEMETRY_<NAME>_COUNT` and a\n`RAC_TELEMETRY_<NAME>_VALUES[]` array:\n\n| Table | Count | Wire field | Values |\n|---|---|---|---|\n| `RAC_TELEMETRY_EVENT_TYPE` | 106 | `event_type` | namespaced strings, e.g. `llm.generation.completed`, `auth.succeeded`, `stt.transcription.completed` |\n| `RAC_TELEMETRY_BACKEND` | 22 | `backend` / `framework` | `onnx, sherpa, llamacpp, foundation_models, system_tts, fluid_audio, coreml, whisperkit_coreml, mlx, qhexrt, neurt, tflite, executorch, mediapipe, mlc, pico_llm, piper_tts, swift_transformers, cloud, builtin, none, unknown` |\n| `RAC_TELEMETRY_MODEL_SOURCE` | 3 | `model_source` | `catalog, builtin, user` |\n| `RAC_TELEMETRY_PLATFORM` | 9 | `platform` | `ios, android, macos, windows, linux, web, tvos, watchos, visionos` |\n| `RAC_TELEMETRY_SDK_BINDING` | 9 | `sdk_binding` | `swift, kotlin, flutter, react-native, web, electron, python, cli, cpp` |\n| `RAC_TELEMETRY_BATTERY_STATE` | 4 | `battery_state` | `charging, full, unplugged, unknown` |\n\n**The rule, verbatim from `add_vocabulary_string()` in `telemetry_json.cpp`:** if a value is\nnon-null but not present in its table, the field is dropped to `null` and the *rest of the\nevent still ships*. This is by design (a bad `backend` string must not cost the whole\nobservation) but it means:\n\n- The backend never sees the bad value, never 422s, never quarantines anything — there is\n  no error path anywhere for this. A field just isn't there.\n- The only client-side signal is a `RAC_LOG_WARNING(\"Telemetry\", \"Dropping %s: '%s' is not\n  in the published vocabulary\", ...)` log line — easy to miss unless you're specifically\n  watching debug logs.\n- `platform`, `sdk_binding`, and `battery_state` are stamped by the manager itself (§1), not\n  supplied by a binding's call site, so in practice only `backend`/`framework` (from the\n  engine/model layer) and `event_type` (only reachable via the proto path, checked at CI\n  time — see §5) are realistic places a new value can silently disappear.\n- `test_telemetry_extraction.cpp`'s vocabulary block (`\"vocab-1\"`/`\"vocab-2\"` cases) is the\n  concrete regression guard: it sends `framework = \"LlamaCpp\"` (a real spelling that used to\n  reach production) and asserts the string never appears on the wire while `model_id` still\n  does. If you touch `add_vocabulary_string()` or add a new vocabulary-gated field, extend\n  that test rather than trusting a manual check.\n\nHistorical motivation (from the header's own comment): before this table existed, stored\nproduction rows had four spellings of `llama.cpp`, three of ONNX, and binding names like\n`\"react-native\"` recorded as a `platform` — this file exists specifically to make that class\nof drift fail in this repo instead of being discovered in a database nobody was watching.\n\n## 3. Presence vs. absence: the \"measured zero\" contract\n\nBefore the fix documented in `telemetry_json.cpp`'s header, extractors read proto3 scalar\nfields directly — which report `0` for a field that was never set — and the old serializer\n(`add_int`/`add_double`) *skipped* zero values to keep payloads small. Result: a genuinely\nmeasured `output_tokens = 0` (an empty completion) and an unmeasured `tokens_per_second`\nboth arrived on the wire as absent, indistinguishable from each other.\n\nThe fix is `has_x`-gated serialization. Two helper families in `telemetry_json.cpp`:\n\n- `add_int_or_null(key, value, is_valid)` / `add_double_or_null(key, value, is_valid)` —\n  emit the real value (including `0`) when `is_valid` is true, else the JSON literal `null`.\n  Non-finite doubles (`NaN`/`Inf`) always degrade to `null` regardless of `is_valid`, because\n  JSON has no such literal and one bad value must not corrupt the whole batch.\n- The `is_valid` argument is a real `has_x`/sentinel check per field, not a blanket flag —\n  e.g. `payload->has_processing_time_ms != RAC_FALSE`, `payload->total_memory > 0`,\n  `payload->battery_level >= 0`.\n\n**What to check to confirm this stayed fixed** (this is exactly what\n`test_telemetry_extraction.cpp`'s presence block does — see §4): send an LLM completion\nevent with `output_tokens = 0` and `input_tokens = 0` explicitly set, and\n`tokens_per_second`/`context_length` left unset. Assert the wire body contains\n`\"output_tokens\":0` and `\"input_tokens\":0` as literal numbers, and `\"tokens_per_second\":null`\n/ `\"context_length\":null` — not all four as `null`, and not all four as `0`. If a future\nrefactor reverts to plain `add_int`/`add_double` for any of these fields, that regression\ntest is the thing that catches it; grep `telemetry_json.cpp` for `add_int(` / `add_double(`\n(the non-`_or_null`/non-`_always` forms) if you suspect a field regressed — those two still\nsilently skip zero by design and must never be used for a field where `0` is meaningful.\n\n## 4. Build, test, and what each telemetry test asserts (macOS)\n\n```bash\ncmake --preset macos-debug\ncmake --build build/macos-debug\nctest --preset macos-debug --output-on-failure -R 'telemetry|device_identity'\n```\n\nOr the non-preset form (`core/AGENTS.md`'s documented path):\n\n```bash\ncmake -B build -DRAC_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Debug\ncmake --build build\nctest --test-dir build --output-on-failure\n```\n\n`telemetry_extraction_tests` and `device_identity_tests` are registered in\n`core/tests/CMakeLists.txt`; `rcli_telemetry_live_tests` is registered separately in\n`rcli/tests/CMakeLists.txt`. All three are hand-rolled assertion runners — `CHECK()` in\nsome files, `ASSERT_EQ`/`ASSERT_TRUE` (from `core/tests/test_common.h`) in others — never\nGoogleTest:\n\n| ctest name | Binary | What it asserts |\n|---|---|---|\n| `telemetry_extraction_tests` | `test_telemetry_extraction` | Full proto→JSON pipeline per modality (LLM token counts/`tokens_per_second`, STT NaN-confidence never leaks as the string `\"nan\"`, TTS `characters_per_second`, embeddings `total_tokens`/`batch_size`/`embedding_dimension`, LoRA failure-path fields, RAG retrieval counts, VLM image/timing fields, live device-state stamping); the presence/measured-zero contract (§3); the closed-vocabulary drop rule (§2); and an HTTP-retry backoff (a failed batch is re-sent only after >5s, never before). Requires `RAC_HAVE_PROTOBUF` — the test binary itself no-ops with exit 0 if commons wasn't built with protobuf support. |\n| `device_identity_tests` | `test_device_identity` | Every branch of `rac_device_get_or_create_persistent_id()`'s resolution chain (§6) against a mocked platform adapter — cache hit, vendor-ID fallback, fresh-UUID generation, and that a `secure_set` failure doesn't silently drop the ID. |\n| `rcli_telemetry_live_tests` | `test_rcli_telemetry_live` | **Opt-in, always registered but a no-op by default.** Runs only with `--live` *and* `RUNANYWHERE_BASE_URL`/`RUNANYWHERE_API_KEY` set; otherwise prints \"skip\" and exits 0 — a green ctest run for this target proves nothing by itself. When live, it runs `rcli::bootstrap()` (real API-key → device-register → JWT handshake), sends one real event per modality (LLM, embeddings, RAG, VLM, LoRA-failure) through the real HTTP transport, and fails on any non-2xx response — this is the test that would catch a strict-schema `422 extra_forbidden` from field drift against the real V2 endpoints. |\n\nRun it live (§7 has the full local-backend flow):\n\n```bash\nRUNANYWHERE_BASE_URL=https://<backend-origin> RUNANYWHERE_API_KEY=<key> \\\n  ./build/macos-debug/rcli/tests/test_rcli_telemetry_live --live\n```\n\n`test_platform_bridge` (same `CMakeLists.txt`, adjacent target) is the wider companion\ncheck — \"every value a binding supplies must reach the wire\" — covering the platform half\nof the same two flat C structs (`rac_client_info_t`/`rac_device_registration_info_t`) for\nevery SDK at once; run it alongside if you're touching client-info fields rather than\ntelemetry event fields specifically.\n\n## 5. Regenerating the vocabulary header, and the drift gate\n\n`rac_telemetry_vocabulary.h` (§2) is generated from `idl/http/sdk-openapi.json` — the\ndevice-facing OpenAPI subset copied in from the private backend repo. **How that file gets\nupdated on the backend side, and how it crosses into this repo, is the monorepo's\n`openapi-contract` skill's job — read that by name in the other repo rather than\nre-deriving the steps here.** From inside this repo, once `idl/http/sdk-openapi.json` is\ncurrent:\n\n```bash\npython3 idl/codegen/generate_telemetry_vocabulary.py          # regenerate the header\npython3 idl/codegen/generate_telemetry_vocabulary.py --check  # CI drift check; exit 1 if stale\n```\n\n`.github/workflows/telemetry-vocabulary-drift.yml` runs three checks on every push to\n`main`/`development` and on any PR touching `idl/http/**`, the codegen script, or the\ntelemetry source/header dirs:\n\n1. **Generated header is current** — the `--check` command above.\n2. **Every event type the core can emit is in the vocabulary** — a Python check that parses\n   `proto_event_type_string()`'s body out of `telemetry_manager.cpp` via regex, unions in\n   the two prefix-expansion families (`stt.model.*`/`tts.voice.*`/`llm.model.*` and\n   `llm*`/`vlm*` generation suffixes), and diffs against `TelemetryEventType`'s enum in\n   `idl/http/sdk-openapi.json`. Anything emittable but unpublished fails the job.\n3. **Every framework string the core can emit is in the vocabulary** — same technique\n   against `framework_proto_to_string()` and `framework_to_string()` vs. `TelemetryBackend`.\n\n**A documented drift you will hit if you trust the wrong file**: `idl/http/README.md`'s own\n\"What it pins\" table names the schema for the `framework`/`backend` field as\n`TelemetryFramework` — that's stale. The actual generator\n(`idl/codegen/generate_telemetry_vocabulary.py`'s `VOCABULARIES` dict) and the header it\nproduces both say `TelemetryBackend`. Trust the generator and the generated header's own\n`// Published as X` comments over that README table.\n\n## 6. Per-binding device identity: where it's created, where it's persisted\n\nThe resolution chain itself is centralized in commons —\n`core/src/infrastructure/device/device_identity.cpp`\n(`rac_device_get_or_create_persistent_id`, tested by `device_identity_tests`, §4) — and is\nthe same fixed order for every binding:\n\n1. `adapter.secure_get(\"com.runanywhere.sdk.device.uuid\")` — if found and non-empty, use it,\n   full stop.\n2. `adapter.get_vendor_id` (if the platform adapter provides one) — use it, then persist it\n   via `secure_set` so step 1 hits next boot.\n3. Generate a fresh RFC-4122 v4 UUID, persist it via `secure_set`.\n\nWhat differs per binding is **only** the `secure_get`/`secure_set` backing store — an\nunstable backing store (one that doesn't survive a real app restart, or that resolves to a\ndifferent physical location than the read path expects) is exactly the bug class that\ninflates device counts, because step 3 fires again on every \"cache miss\":\n\n| Binding | Backing store | Where |\n|---|---|---|\n| Swift (iOS/macOS) | Keychain, service `com.runanywhere.sdk` | `bindings/swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+PlatformAdapter.swift` (`platformSecureGetCallback`/`platformSecureSetCallback`) |\n| Kotlin (Android) | Android Keystore AES-256-GCM key, ciphertext in a no-backup app file (not `SharedPreferences`) | `bindings/kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/security/AndroidKeychainManager.kt` (`NoBackupCiphertextStore`), wired through `CppBridgePlatformAdapter.PlatformSecureStorage` |\n| Flutter | Native Keychain/Keystore helper via FFI symbols `ra_flutter_secure_storage_{store,retrieve,delete}` | `bindings/flutter/packages/runanywhere/lib/native/dart_bridge_secure_storage.dart`; the device-UUID key constant is duplicated (deliberately, per its own comment) as `_keyDeviceUUID = 'com.runanywhere.sdk.device.uuid'` in `dart_bridge_device.dart` |\n| React Native (Android) | `SecureStorageManager` (native Android side) via `PlatformAdapterBridge.kt`'s `secureGet`/`secureSet` | `bindings/react-native/packages/core/android/src/main/java/com/margelo/nitro/runanywhere/PlatformAdapterBridge.kt` |\n| Web | `localStorage`, key `rac_sdk_plaintext_com.runanywhere.sdk.device.uuid` — **plaintext**, no OS-level encryption equivalent exists in a browser; restricted by convention to non-sensitive IDs | `bindings/web/packages/core/src/runtime/PlatformAdapter.ts` (`registerSecureGet`/`registerSecureSet`); also has a separate `stableVendorId()` fallback under key `rac_sdk_plaintext_vendor_id` |\n| Electron (macOS/Linux) | Flat file under a secure directory, path-traversal-guarded (`fs::path(key)` must not be absolute or contain `..`) | `bindings/electron/native/posix_platform_adapter.cpp` (`posix_secure_get`/`posix_secure_set`) |\n| Electron (Windows) | Windows DPAPI (`CryptProtectData`, current-user scope, no UI) | `bindings/electron/native/win32_platform_adapter.cpp` (`win_secure_get`/`win_secure_set`) |\n\nThe key string itself — `com.runanywhere.sdk.device.uuid` — is intentionally the same\nliteral across Swift, Flutter, and RN (each source comments cross-reference the other two by\nname), so grepping for it is a fast way to confirm a new binding is reading/writing the\n*same* key it thinks it is, not a typo'd sibling.\n\n**How to verify stability across a real restart** (not just \"the code looks right\"):\n\n1. Run the binding once, capture the emitted `device_id` from a real request body (§7) or\n   from `POST /api/v1/devices/register`'s response.\n2. Fully terminate the process (not just background it — on mobile this matters, a\n   backgrounded app can keep its in-memory device-id cache alive without ever touching\n   the read path again).\n3. Relaunch and repeat step 1.\n4. The `device_id` must be byte-identical. If it changed, the break is almost always one of:\n   the backing store not surviving process death (e.g. an in-memory-only stub used during\n   development), a key-name mismatch between the write and read paths, or (Android\n   specifically) `Context.deleteSharedPreferences` being invoked on the wrong file — see\n   `AndroidKeychainManager.kt`'s constructor, which deletes a hardcoded *legacy* prefs name\n   on every construction; if that name were ever widened to match the real store, every\n   Android launch would look like a fresh device.\n\nTwo-in-one launches on the *same physical device* (e.g. two emulator instances, or a\nsame-device reinstall vs. a real fresh install) are a different, real-world failure mode\nfrom a code bug — don't conflate an intentional reinstall's fresh ID with instability.\n\n## 7. Point a local build at a local backend and prove an event landed\n\n`rcli` is the fastest way to exercise the full wire path without a mobile/browser\ntoolchain — it links `rac_commons` directly and speaks real HTTP via curl.\n\n```bash\ncmake --preset rcli-macos-release   # or rcli-linux-release on Linux\ncmake --build build/rcli-macos-release --target rcli -j \"$(sysctl -n hw.logicalcpu)\"\n```\n\n**Keyless (development environment) — no API key, works against any backend that allows\nanonymous V2 telemetry:**\n\n```bash\n./build/rcli-macos-release/rcli/rcli --environment development \\\n  --base-url http://127.0.0.1:8000 \\\n  telemetry blast --processing-ms 42.5 --session-id my-check \\\n  --input-tokens 128 --output-tokens 256\n```\n\nThis is exactly the shape `scripts/ci/oss_keyless_telemetry_blast.sh` runs in CI against the\npublic staging backend — `--environment development --base-url <origin> telemetry blast\n...` with no `--api-key`. `telemetry blast` sends one representative event per modality (all\n12) and prints a `MODALITY | RESULT | STATUS | RECEIVED | STORED | SKIPPED` table parsed\ndirectly from the backend's own batch response — **the `STORED` column is authoritative\nserver-side confirmation**, not just an HTTP 2xx. `run_telemetry_blast()`'s own exit-code\nlogic (`rcli/src/commands/cmd_telemetry.cpp`) does fold `stored >= count` into its overall\npass/fail, so a genuine storage shortfall on any modality does make the process exit\nnon-zero, not just a transport error — but the exit code is one bit for all 12 modalities\ncombined, so a non-zero exit still requires reading the table to know *which* modality\nfailed and *why* (a `network error` status is a transport problem; an `HTTP 200` row with\n`STORED` under `RECEIVED` is a row-level rejection, e.g. a vocabulary value the backend\ndidn't recognize — a real bug to chase, not the same failure class at all).\n\n**Authenticated (production shape) — for exercising the login handshake too:**\n\n```bash\n./build/rcli-macos-release/rcli/rcli --environment production \\\n  --base-url https://<backend-origin> --api-key <key> \\\n  auth login\n./build/rcli-macos-release/rcli/rcli --environment production \\\n  --base-url https://<backend-origin> --api-key <key> \\\n  telemetry emit --modality llm --count 3 --input-tokens 128 --output-tokens 256\n```\n\n`auth login` only has a path in `production` — `development`'s keyless mode has no login\nstep by design (see `rcli/src/commands/cmd_auth.cpp`'s own subcommand help text).\n\n**To inspect the actual request body**, not just the summary table, add `--json` for\nmachine-readable CLI output and `-v` for debug logging (the debug log includes the\nserialized JSON per batch before it goes on the wire) — or point `--base-url` at a throwaway\nlocal capture endpoint (any script that logs the raw POST body) before pointing it at a real\nbackend, to sanity-check field shape offline first.\n\n**If you have the private backend repo checked out locally too**, running it yourself\n(`cd backend && uv run uvicorn main:app --reload`, per that repo's own README) and pointing\n`--base-url` at `http://127.0.0.1:8000` gets you the strongest possible evidence: query the\nSQLite/Postgres row directly after a blast and diff it field-for-field against what you sent\n— the monorepo's own `telemetry-e2e` skill (its integration suite, hermetic pytest fixtures,\nand `test_telemetry_conformance.py`'s vocabulary/presence/identity assertions against stored\nrows) is the authoritative reference for that side; don't re-implement its DB assertions\nhere, just point at it.\n\n## Evidence bar, restated\n\n- Compiling `rcli` or a binding: **not evidence**.\n- `ctest -R telemetry_extraction_tests` green: **not evidence of a real call site** — only\n  of the serializer in isolation.\n- `rcli telemetry blast` exiting 0: real signal (its exit code does fold in the backend's\n  `stored >= count` check, §7) but still worth pairing with the per-modality\n  `STORED`/`SKIPPED` table in your report — the exit code can't tell you *which* modality\n  or *why* a shortfall happened.\n- A captured request body showing the right endpoint, the right keys, correct\n  presence/absence per §3, and no off-vocabulary values per §2 — **evidence of correct\n  emission**.\n- That request body's values matching a stored row (via the backend's own\n  `events_stored` count at minimum, a direct DB read at best) — **evidence of correct\n  end-to-end delivery**, the only tier worth reporting as \"telemetry works.\"\n",".agents/skills/sdk-test-starters/SKILL.md":"---\nname: sdk-test-starters\ndescription: Run a REAL end-to-end inference smoke test (model download + load + generate, with visible output) on every RunAnywhere starter app — iOS, macOS/Swift, Android, Web, Electron — after a runanywhere-sdks release. Build success alone is never sufficient evidence.\n---\n# SDK Test Starters\n\nPost-release validation runbook for the five RunAnywhere SDK consumer apps. This\nskill exists because \"it built\" has been reported as \"tested\" before, and that is\nnot evidence of anything except that the compiler ran.\n\n## 0. The evidence bar (applies to every app, no exceptions)\n\nFor each app you must produce, and be able to state explicitly:\n\n1. **Version proof** — print/log the actual resolved SDK version from inside the\n   running app (a startup log line, a version screen, an `swift package show-dependencies`\n   / `./gradlew dependencies` / `npm ls @runanywhere/...` output, etc). Never assume\n   a version bump \"took\" just because you edited a manifest.\n2. **Real model acquired** — a real (small) model downloaded fresh, or confirmed\n   already cached, by the app itself (not a file you hand-placed unless that IS the\n   app's supported flow).\n3. **Load** — the model actually loads (watch for a load-succeeded log/state, not\n   just \"no crash\").\n4. **One real inference call** — LLM `generate()` is almost always the easiest\n   modality to drive; use it unless another modality is genuinely less work for\n   that specific app.\n5. **Visible output** — capture the actual generated text (or audio/image) and\n   quote it in your report. \"It responded\" is not enough — show what it said.\n6. **Logs reviewed** — read the console/log stream for errors and warnings and\n   explicitly explain any that appear. Do not silently drop a stack trace because\n   the app \"still produced output.\"\n\nReporting an app as tested from a build/compile success alone is a skill violation.\nIf you could not get past build, that is a BLOCKED report (see §8), not a PASS.\n\n## 1. Locating each app's real local checkout\n\nThis machine accumulates many differently-named copies of the same repos over time\n(worktrees, experiment clones, old branches, scratch dirs). **Never trust a\ndirectory name.** Before touching anything in a candidate directory:\n\n```bash\ngit remote get-url origin          # must be https://github.com/RunanywhereAI/<repo>.git\ngit status --short --branch        # what branch, what's dirty, is it someone else's WIP?\n```\n\nAs of this writing, the known-good local checkouts for this user are here (verify\nthis each time — do not hardcode it further than \"check this location first\"):\n\n```\n~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-ios       -> RunanywhereAI/runanywhere-ios.git\n~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-android   -> RunanywhereAI/runanywhere-android.git\n~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-web       -> RunanywhereAI/runanywhere-web.git\n~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-electron  -> RunanywhereAI/runanywhere-electron.git\n```\n\nThe macOS/Swift case has no separate consumer repo — see §4, it lives inside\n`~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks` (confirm with\n`git remote get-url origin` there too — this machine also has stray full-monorepo\nclones elsewhere, e.g. under `~/development/hard/ambient/`, that are NOT the one to\nuse unless you've verified it's the intended checkout).\n\nThese starter checkouts are commonly sitting on a feature branch, not `main` — e.g.\nat last check `runanywhere-ios` and `runanywhere-android` were on\n`add-gemma4-qwen3.6-qwen3.8-models` and `runanywhere-web` on `add-supertonic-3-tts`.\nThat is fine to test FROM, but say so in your report, and do not `git checkout` away\nfrom a branch that has uncommitted work you didn't create — if `git status --short`\nshows dirty files you don't recognize, stop and ask rather than stashing/resetting.\n\nIf genuinely unsure which directory is real, `git clone` a fresh one into the\nscratchpad rather than guessing — a wrong guess wastes far more time than a clone.\n\n## 2. Cross-repo facts this skill assumes\n\n- SDK version SoT: `core/VERSION` in `runanywhere-sdks`, bumped everywhere by\n  `scripts/release/sync-versions.sh <version>`.\n- Release publish is `.github/workflows/release.yml` (dispatch or `v*.*.*` tag).\n- **Never** treat a build success against `RUNANYWHERE_USE_LOCAL_NATIVES=1` (or any\n  monorepo-local staged artifact) as proof the *public* release works — see §4.\n- QHexRT/QNN material must never appear in a public artifact — release.yml\n  hard-fails on it. If anything you're testing surfaces a QHexRT asset in a public\n  package, stop and flag it, don't route around it.\n\n## 3. iOS — `runanywhere-ios`\n\nSDK pin lives in `Package.swift`:\n\n```swift\n.package(url: \"https://github.com/RunanywhereAI/runanywhere-swift.git\", from: \"0.20.19\")\n```\n\nThis only resolves once `runanywhere-swift` (the generated Swift-only SPM\ndistribution repo — see `~/.claude/skills/sdk-publish/SKILL.md` §4) is\nactually tagged at the target version. Steps:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-ios\n# bump the `from:` version in Package.swift to the target release, then:\nswift package update            # or open in Xcode and let it resolve\n./scripts/build_and_run_ios_sample.sh simulator \"iPhone 16 Pro\"\n```\n\n`./scripts/verify.sh` is the CI-equivalent gate: it resolves the package remotely\nand runs a full `xcodebuild` against the iOS Simulator — use it to prove the\nresolve is clean (it fails if `Package.resolved` ends up dirty, i.e. the committed\npin was stale). `./scripts/smoke.sh` is a fast, non-compiling grep-based check —\nit does NOT count as evidence for this skill's bar, only `build_and_run` +\nwatching real output does.\n\nDrive one real `generate()` call through the app UI (or the equivalent debug\naction if the sample exposes one) against a small downloaded model, then watch the\nruntime log stream for the version line, the model download/load, the generation,\nand any errors:\n\n```bash\nlog stream --predicate 'subsystem CONTAINS \"com.runanywhere\"' --info --debug\n```\n\nMost loggers use `com.runanywhere.RunAnywhereAI`; a few use plain\n`com.runanywhere`; the SDK logs under its own subsystem — the `CONTAINS` match\ncovers all of them. On a physical device use `idevicesyslog | grep \"com.runanywhere\"`\ninstead.\n\n## 4. macOS/Swift — no separate repo, use the in-tree example or rcli\n\nThere is no `runanywhere-macos` consumer repo. Two in-monorepo options, both inside\n`~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks`:\n\n- `bindings/swift/example/` — a minimal SwiftPM example app/executable.\n- `rcli/` — the C++ core's CLI, `rac_*` C ABI consumer.\n\n**The critical gotcha**: the repo's own `Package.swift` is fail-closed toward the\n*real published release* — it resolves the remote `binaryTargets` unless you\nexplicitly opt into locally staged XCFrameworks with\n`RUNANYWHERE_USE_LOCAL_NATIVES=1`. Every doc/script example in this repo sets that\nenv var (because local dev wants fast iteration against in-progress binaries) —\nbut that means copy-pasting those examples verbatim tests your LOCAL staged build,\nnot the actual public release artifact. To prove the published release works:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks/bindings/swift/example\n# DO NOT set RUNANYWHERE_USE_LOCAL_NATIVES — leave it unset so Package.swift\n# resolves the real remote release binaries for the target version.\nswift build\nswift run runanywhere-minimal \"Name three colours.\"\n```\n\nIf the example was previously built/run with the env var set, `swift build` may\nreuse a stale resolve — run `swift package reset` (or delete `.build/`) first if\nyou're not sure which mode the last resolve used.\n\nFor rcli instead (also validates the real published core):\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks\nrcli models download qwen3          # ~639MB, small enough for a quick smoke test\nrcli llm generate --model qwen3 \"Reply with exactly: RCLI WORKS\" --reasoning off\n```\n\n`rcli models list --all` shows the rest of the built-in catalog\n(`whisper-tiny`, `smolvlm2`, `piper`, …) if LLM generate isn't the right modality\nfor what you're verifying.\n\n## 5. Android — `runanywhere-android`\n\nVersion pin: `gradle/libs.versions.toml` has ONE shared catalog entry:\n\n```toml\nrunanywhere = \"0.20.19\"\n...\nrunanywhere-sdk      = { group = \"io.github.sanchitmonga22\", name = \"runanywhere-sdk\",              version.ref = \"runanywhere\" }\nrunanywhere-llamacpp = { group = \"io.github.sanchitmonga22\", name = \"runanywhere-llamacpp\",          version.ref = \"runanywhere\" }\nrunanywhere-onnx     = { group = \"io.github.sanchitmonga22\", name = \"runanywhere-onnx\",               version.ref = \"runanywhere\" }\nrunanywhere-qhexrt   = { group = \"io.github.sanchitmonga22\", name = \"runanywhere-qhexrt-android\",     version.ref = \"runanywhere\" }\n```\n\n**CRITICAL — do not bump this block as one unit.** `runanywhere-qhexrt-android` is\npermanently frozen at whatever version it was last published at before being\nexcluded from the public Maven train; by policy it can never move forward via\npublic Maven again. Verify the actual frozen version FRESH every time (do not\ntrust a remembered number — it doesn't change, but don't skip the check):\n\n```bash\ncurl -s https://repo1.maven.org/maven2/io/github/sanchitmonga22/runanywhere-qhexrt-android/maven-metadata.xml\n```\n\nThe other three artifacts (`runanywhere-sdk`, `-llamacpp`, `-onnx`) CAN and should\nmove to the new release version. This means the single `runanywhere` catalog ref\nmust be **split into two entries** — one at the new version for sdk/llamacpp/onnx,\none pinned to the frozen qhexrt version — not bumped as a block.\n\n**Before trusting a mixed pin works**: verify `RAC_PLUGIN_API_VERSION` (the\nplugin ABI version constant in the C++ core, exposed identically to Kotlin) is\nunchanged between the old public SDK version and the new one. If it changed, mixing\na newer sdk/llamacpp/onnx with the frozen qhexrt version is NOT safe — this is a\nhuman decision (needs either an ABI-compat shim or dropping qhexrt from this build),\nnot something to force through.\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-android\n./gradlew :app:assembleDebug\n./gradlew :app:installDebug        # installs to a connected emulator/device\nadb logcat | grep -i runanywhere   # or a more specific tag if the app uses one\n```\n\nLaunch the app, drive one real `generate()` call against a small downloaded model\n(the app's own model picker/download flow), confirm the resolved SDK version is\nlogged, and review logcat for errors before reporting a PASS.\n\n## 6. Web — `runanywhere-web`\n\n`package.json` has 4 `@runanywhere/*` deps pinned with a caret range:\n\n```json\n\"@runanywhere/proto-ts\":    \"^0.20.19\",\n\"@runanywhere/web\":         \"^0.20.19\",\n\"@runanywhere/web-llamacpp\":\"^0.20.19\",\n\"@runanywhere/web-onnx\":    \"^0.20.19\"\n```\n\nBump all 4 to the new version, then:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-web\nnpm install\nnpm run build\nnpm run dev      # vite --host localhost --port 3000 --strictPort\n```\n\nThe dev/preview server is what supplies COOP/COEP headers required for\n`SharedArrayBuffer` — WASM inference will silently fail (or refuse to init\nthreads) without them, so don't swap in a bare static file server.\n\n`~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks/bindings/web/AGENTS.md`\nhas the authoritative Validation section (its own heading is `## Validation`) —\nfull checklist:\n\n1. Fresh browser context.\n2. Example app served with COOP/COEP headers.\n3. Model download.\n4. Model load.\n5. Real browser inference for the target modality.\n6. Logs/screenshots reviewed.\n\nIts \"Required release gates\" (run before browser validation) from the same repo:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks/bindings/web\nnpm run typecheck && npm run lint && npm run test && npm run build\nnpm run test:browser\nnpm run test:browser:release\ncd example && npm run typecheck && npm run build\n```\n\nStatic gates passing is not the smoke test — you still need an actual browser\nwatching real inference happen against the starter app. Prefer a real browser\nautomation tool (`claude-in-chrome` MCP tools, or Playwright — the maintained\nsuites under `bindings/web/tests/browser/` already use `playwright.config.ts`)\nover curling the served page: WASM inference genuinely needs a browser JS engine.\nLoad the served starter app, download/load a small model, trigger generate, and\nread the actual rendered output text plus the browser console — do not infer\nsuccess from an HTTP 200 on the page load.\n\n## 7. Electron — `runanywhere-electron` (SPECIAL CASE)\n\n**Do not assume a fresh SDK publish is available for this app.** Electron's own\nnpm packages (`@runanywhere/electron`, `-llamacpp`, `-onnx`, `-sherpa`, `-qhexrt`,\n`-neurt`) are on a SEPARATE, manual-publish pipeline — NOT part of `release.yml`\n(grep confirms zero mentions of these package names there). This separation is a\n**confirmed, deliberate** repo-owner decision, not a gap. Check\n`~/.claude/skills/sdk-publish/SKILL.md` §6 before assuming anything moved.\nElectron is **no longer WIP on a branch** — the old \"in-progress on\n`smonga/electron_upgrade`\" note is gone from the root `AGENTS.md`/`CLAUDE.md`\nentirely as of this checkout; `bindings/electron/` is a fully merged SDK on `main`.\nAs of `fix/electron-cicd-staging-url-npu-ane`,\n`.github/workflows/electron-native-package.yml` builds every backend for real and\npackages every tarball — but still does not publish; verify against a live run\nrather than assuming build automation status either way.\n\nTest the starter against whatever version it is CURRENTLY pinned to unless a human\nhas explicitly confirmed a newer Electron publish is ready and intended:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-electron\ncat package.json | grep '@runanywhere/electron'   # confirm current pin before touching it\n```\n\n**`@runanywhere/electron-qhexrt` being public on npm is a CONFIRMED, settled\npolicy decision** (repo owner, 2026-08-20), not a contradiction to flag — it\ncontains real Hexagon NPU binaries (`libQnnHtpV81Skel.so`, `QnnHtp*.dll`,\n`rac_commons.dll`, `runanywhere_qhexrt.dll`) on purpose. The QHexRT-stays-private\npolicy enforced elsewhere in this repo family is specifically about the\nGitHub-Release-side asset set (Android etc.), a different channel. Only re-raise\nthis if you find evidence the policy changed.\n\nTwo layers to actually exercise, do not conflate them:\n\n**(a) Cross-platform Electron JS + macOS native addon** — buildable and testable\nlocally on this Mac right now:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-electron\nnpm install\nnpm run build   # or the app's documented dev/start script — check package.json scripts\nnpm start        # or equivalent — launch the actual Electron app\n```\n\nDrive one real `generate()` call in the running app, watch its console/log output\nfor the resolved SDK version, model download, load, and generation, and read any\nerrors rather than ignoring them.\n\n**(b) Genuine Windows-native behavior** — a macOS build proves NOTHING here\nregardless of how clean the cross-platform code looks; the native addon and any\nWindows-only DLLs must be exercised on real Windows.\n\nThe team keeps a **Windows ARM64 test machine reachable over SSH**. Its hostname,\nuser, key, and VPN/mesh node name are operator-specific and deliberately not\nrecorded in this repo — read them out of the operator's own `~/.ssh/config` (look\nfor the Windows/ARM64 host entry) and use that alias below wherever `$WIN_BOX`\nappears. If no such entry exists, the box is not configured for you: say so and\nask, rather than guessing at a hostname.\n\n```bash\nWIN_BOX=<the ssh alias from ~/.ssh/config>\n```\n\n**Always check reachability first, never just try SSH and retry on timeout.** The\nbox lives on a private mesh VPN and is frequently asleep, so start by asking the\nVPN client whether the node is up (e.g. `tailscale status`, matching on the host\nentry's `HostName`) rather than opening a connection.\n\nIf the node reports `offline, last seen ...`, the box is asleep/powered off.\n**A connection timeout here means \"device asleep,\" not \"config is wrong.\"** Tell\nthe human and ask them to wake it; do not loop retrying SSH. Only once the node\nshows a live/idle state, confirm with:\n\n```bash\nssh -o ConnectTimeout=8 \"$WIN_BOX\" \"whoami\"\n```\n\nThen, with the repo already cloned there:\n\n```bash\nssh \"$WIN_BOX\" \"cd <repo-path-on-that-box>; git pull; npm install; npm run build\"\n# then run whatever launches the Windows Electron build and drive a real generate call\nscp \"$WIN_BOX\":<remote-log-or-artifact-path> ./local-copy   # pull back evidence to inspect\n```\n\nDo not claim Windows-specific validation happened unless you actually obtained a\nshell on that box and ran something real there — a clean macOS build is not\nsubstitute evidence.\n\n**`node_modules/electron` on this box silently tracks whichever arch last ran `npm\ninstall`, not which Node launches the test.** The x64 (llamacpp/onnx/sherpa) and\narm64 (QHexRT) lanes share ONE `runanywhere-electron` checkout/`node_modules` tree.\nPrepending an x64 Node to `PATH` and running `npx playwright test` does NOT make\nElectron itself x64 — `require('electron')` always resolves the single installed\n`node_modules/electron/dist/electron.exe`, and whichever arch that binary actually\nis determines which `prebuilds/win32-<arch>/` directory the running app looks in,\nregardless of `scripts/use-sdk.mjs local`'s staging (which itself is arch-aware\nbased on the *launching* Node, so it can correctly report \"staged win32-x64\" while\nthe process that will actually run is arm64 — a silent mismatch). Verify with:\n`powershell -Command \"$p=[IO.File]::ReadAllBytes('...\\electron.exe'); $peOff=\n[BitConverter]::ToInt32($p,60); [BitConverter]::ToUInt16($p,$peOff+4)\"` — `43620`\n(`0xAA64`) is ARM64, `34404` (`0x8664`) is x64. If it's the wrong arch (e.g. a prior\nsession's `npm install` for the other lane silently overwrote it), a genuine re-test\nof the other lane needs a FULLY SEPARATE checkout/`node_modules` tree installed\nunder the matching-arch Node — don't assume PATH-prepending alone is sufficient, and\ndon't trust `use-sdk.mjs status`'s \"platform: win32-x64\" output as proof the running\nElectron process is actually x64.\n\n**NPU hardware is a scarce, single-owner resource — serialize, never parallelize.**\nThis box has ONE physical Hexagon NPU. `engines/qhexrt/qhexrt_session.cpp`'s\n`session_open()` has a load-bearing comment on this exact failure mode: the HTP\nexposes a small number of protection domains with a per-PD context ceiling, so a\n*second* concurrent model load fails with `RAC_ERROR_BACKEND_INIT_FAILED`\n(\"Backend initialization failed\") — a fast (tens-of-ms), generic-looking failure\nthat is easy to mistake for a real regression. Confirmed once tonight: two agents\neach independently exercising QHexRT on this box at the same time produced exactly\nthis failure. Before concluding a QHexRT test genuinely failed, confirm nothing\nelse (another agent, a leftover Electron process, a stuck previous test run) is\nalso holding an NPU session open on this box — check for lingering `electron.exe`/\n`node.exe` processes from a prior run and kill them if orphaned, and never launch\ntwo NPU-touching verification passes concurrently on this machine.\n\n**\"Backend initialization failed\" has (at least) three distinct root causes —\ndon't stop at PD exhaustion.** Confirmed on 2026-08-17 after PD-exhaustion and\nstaleness were both ruled out (fresh rebuild, source-identical prebuilt, no other\nprocess holding a session): the *actual* bug was two compounding issues, both now\nfixed/documented —\n1. **Real source bug, fixed in PR #733**: `qhexrt_session.cpp`'s `runtime_acquire()`\n   called `qhx_runtime_create(nullptr, nullptr)` on every non-Android platform,\n   trusting the OS default DLL search order to find `QnnHtp.dll`/`QnnSystem.dll`.\n   On Windows that order never includes a dynamically-loaded sibling plugin's own\n   directory (`LOAD_WITH_ALTERED_SEARCH_PATH`, used to load `runanywhere_qhexrt.dll`\n   itself, only widens the search for *that* load call, not later `LoadLibrary`\n   calls made by code already running inside it) — so the QNN DLLs were invisible\n   even sitting right next to the plugin. Fixed by resolving the plugin's own\n   module directory (`GetModuleHandleExA`+`GetModuleFileNameA`) and passing\n   explicit paths, mirroring Android's existing `ADSP_LIBRARY_PATH` resolution in\n   the same file.\n2. **Operational staleness, NOT a script bug**: even after (1), the QNN runtime\n   DLLs actually staged into `@runanywhere/electron-qhexrt`'s\n   `prebuilds/win32-arm64` (`node_modules/.../electron-qhexrt/prebuilds/win32-arm64/`)\n   were from an old QAIRT build (~2.45.x vintage, Dec 2025 files) while the box's\n   installed QAIRT was 2.48.0.260626 — a real ABI-relevant version mismatch.\n   `bindings/electron/scripts/bundle-native.ts`'s `stageQnnRuntime()` just mirrors\n   whatever `RA_QNN_RUNTIME_DIR` points at with **no version check** — it's\n   correct-as-written; someone had pointed it at a stale neurun-side\n   `qairt-runtime-248`-named folder that was never refreshed. Fix: rebuild that\n   flat dir from the CURRENT QAIRT SDK's `lib/aarch64-windows-msvc/` (host DLLs:\n   `QnnHtp.dll`, `QnnHtpPrepare.dll`, `QnnHtpV81Stub.dll`,\n   `QnnHtpV81CalculatorStub.dll`, `QnnSystem.dll`) + `lib/hexagon-v81/unsigned/`\n   (DSP side: `libQnnHtpV81Skel.so`, `libqnnhtpv81.cat`), point\n   `RA_QNN_RUNTIME_DIR` at it, and re-run\n   `npm run bundle:native -- --package=qhexrt` from `bindings/electron/`.\n   **The already-published `@runanywhere/electron-qhexrt` npm package (as of\n   0.20.24) almost certainly ships the same stale DLLs** — this needs a\n   human decision (republish/patch) before claiming the published package works\n   on Windows NPU, separate from the source fix in PR #733.\n3. **PD exhaustion** (the pre-existing entry above) remains real and still applies\n   when two sessions genuinely contend for the one physical NPU — but it is not\n   the only explanation, and the log line to actually check first is the native\n   one: subscribe via `window.runanywhere.logging.records()` (after\n   `logging.setDebugMode(true)`) or add a temporary main-process log capture —\n   `app.process().stdout`/`stderr` do NOT carry it, because the addon runs in a\n   separate `utilityProcess.fork()` host, not the Electron main process, and\n   `RAC_LOG` is not an env var the logger reads (it does nothing). The generic\n   `[QHexRT] qhx_runtime_create failed (QNN libs unavailable?)` vs.\n   `qhx_model_load failed: ... N NPU model(s) already resident` log text is what\n   actually distinguishes cause (1)/(2) from cause (3) — always get that line\n   before diagnosing further.\n\n**Workflow gotcha:** if you orchestrate this via the Workflow tool, its agent()\ncalls do NOT reliably block-wait on a long-running remote build the way a direct\npoll loop does — observed once tonight where a workflow's own agents spawned an\ninternal Monitor/poller for the build log and then returned immediately,\nreporting \"still waiting,\" which let the whole workflow finish with nothing\nactually verified. For \"wait N tens-of-minutes for a real native build to\nfinish,\" use the top-level Monitor tool yourself (a shell poll loop over SSH\nchecking for a terminal BUILD_SUCCEEDED/BUILD_FAILED marker line) rather than\ndelegating the wait itself into a workflow agent's own judgment.\n\n### The Windows box's actual layout, and why it's more than a cross-platform check\n\n**This is a Hexagon-NPU-equipped Windows machine** (Snapdragon-class ARM Windows\ndevice, Copilot+ PC), not just a generic x86 Windows box. That makes it the one\nplace a QHexRT/Hexagon backend build+test can produce real device-only truth for\nWindows — matching the parent workspace's own \"x86 is a simulator, device-only\ntruth\" rule (see the top-level `Qualcomm/CLAUDE.md` and `QHexRT/CLAUDE.md`). Don't\ntreat this box as merely a Windows Electron smoke-test target; it's the only real\nHexagon NPU test rig reachable for this platform.\n\nPer the user (2026-08-17), under `~/Downloads/RunAnywhere/` on that box there are\nthree sibling repo checkouts at the same level — **verify each on connect, don't\nassume paths without checking, since this is relayed secondhand and may have\ndrifted**:\n- `runanywhere-sdks` — this monorepo, presumably already cloned; `git fetch && git\n  log -1 origin/main` to see how far behind it is before doing anything.\n- `runanywhere-electron` — the Electron starter app (same repo as\n  `RunanywhereAI/runanywhere-electron` elsewhere in this skill).\n- a third repo referred to verbally as \"the neuron/new-run inference engine for the\n  Hexagon NPU\" — almost certainly **`neurun`**, the QHexRT/NeuRT split-out repo\n  (see memory: \"neurun component split merged — QHexRT/ + NeuRT/ + shared/, root is\n  a closed 20-entry set enforced by a test\"). Confirm the actual directory name and\n  remote (`git remote get-url origin`) on connect rather than guessing from the\n  transcription.\n\n**Full task on this box (per explicit user instruction, not just a smoke test):**\n1. `git pull` (or fetch+fast-forward) all three repos to latest `main`.\n2. Rebuild the QHexRT/Hexagon NPU backend from `neurun` against this device's real\n   Hexagon NPU — this is the one environment where that build+run is meaningful\n   proof, not a simulator claim.\n3. Bump Electron's native dependencies to their latest versions — llama.cpp,\n   sherpa-onnx, ONNX Runtime, and whatever else `bindings/electron` pins — across\n   the repos involved. Treat this as a real dependency-bump task (check for\n   breaking API changes, don't just edit a version string and assume it builds).\n4. Update the model catalog for **all 5 starter apps across all 4 consumer repos**\n   (iOS, Android, Web, Electron — plus whatever the \"5th app\" split means for\n   macOS/Swift, see the rest of this skill) so the catalog is consistent everywhere,\n   not just on this Windows box.\n5. Run a real end-to-end inference pass on this box specifically (not just build\n   success) — both the general Electron/llama.cpp path AND the Hexagon NPU path via\n   `neurun`/QHexRT.\n6. Open PRs for whatever changed, in each affected repo. Do not merge them —\n   the user explicitly said they'll review.\n\nThis is real engineering work (native dependency bumps + an NPU backend rebuild),\nnot a mechanical version-string edit — expect it to take real time and to\ngenuinely need `QHexRT/CLAUDE.md` and `neurun`'s own docs read in full before\ntouching that backend, per the parent workspace's own instructions.\n\n## 8. Reporting\n\nFor each of the 5 apps (iOS, macOS/Swift, Android, Web, Electron), state exactly\none of these two outcomes — never blur them:\n\n- **FULLY VERIFIED** — with concrete evidence: the version string actually\n  resolved/logged, the model used, and the actual output produced (quote it).\n- **BLOCKED** — with the exact concrete reason, e.g.: the Windows ARM64 box's\n  mesh-VPN node is offline; Android emulator unavailable in this environment; App Store /\n  notarization credential missing; `runanywhere-swift` not yet tagged at target\n  version so `swift package update` can't resolve; electron-qhexrt policy conflict\n  needs a human call before proceeding further.\n\nA clearly reported partial/blocked result is far more valuable than an optimistic\nsummary that overstates what was actually observed. If you hit the known Electron\nqhexrt npm-publish policy conflict (§7), call it out in the report even if it\nwasn't the thing blocking your specific test — it's a standing issue every release\nnight should re-surface until a human resolves it.\n\n## Credentials referenced but never printed by this skill\n\nThese exist and are checked for presence only — never print their values:\n\n- Maven Central: `mavenCentral.username` / `mavenCentral.password`,\n  `signing.gnupg.keyName` / `signing.gnupg.passphrase` in `~/.gradle/gradle.properties`.\n- GPG signing key `CC377A9928C7BB18` — check with\n  `gpg --list-secret-keys --keyid-format LONG`.\n- macOS notarization profile `runanywhere-notary` — check with\n  `xcrun notarytool history --keychain-profile runanywhere-notary`; codesign\n  identity `Developer ID Application: RunAnywhere, Inc` — check with\n  `security find-identity -v -p codesigning`.\n- npm — `npm whoami` should already show a logged-in user.\n\nNone of the app-level smoke tests in this skill require touching these directly;\nthey matter only if a test path requires a fresh distro-repo publish (e.g.\n`runanywhere-swift` not yet tagged) — that's a separate concern of the\n`sdk-publish` skill, not this one.\n",".agents/skills/engine-plugin-authoring/SKILL.md":"---\nname: engine-plugin-authoring\ndescription: Step-by-step recipe for adding a new engine plugin under runanywhere-sdks/engines/, or adding a modality (e.g. TTS) to an existing engine (llamacpp, sherpa, onnx, cloud, mlx, neurt, qhexrt). Use when the task is \"add engine X\", \"wire up backend Y\", or \"make engine Z also serve <modality>\".\n---\n\n# Engine plugin authoring (`engines/`)\n\nTwo related runbooks: adding a brand-new engine plugin, and adding a modality to one\nthat already exists. Read `engines/AGENTS.md` first for the concepts these steps\nassume (op-table vtable, manifest, the valid-engine checklist, the 4-file skeleton,\nthe engine↔runtime rule). This skill is the \"how\", that file is the \"what/why\".\n\n**Invariant that applies to both procedures:** never invent a new modality just to\nship an engine — fill an existing `rac_engine_vtable_t` slot. Adding a brand-new\n*primitive* (a new `RAC_PRIMITIVE_*` + vtable slot) is a commons ABI change, covered\nby `core/AGENTS.md` → \"Adding a new capability interface\", not an engine change.\n\n---\n\n## Procedure A — add a new engine\n\nMirror **sherpa** (multi-modality, standalone `.so`) or **cloud** (single modality,\nno runtime) as your template; both are the cleanest references for the 4-file\nskeleton described in `engines/AGENTS.md`.\n\n1. **Create `engines/<name>/`** with the 4-file skeleton: `rac_plugin_entry_<name>.cpp`,\n   `rac_backend_<name>_register.cpp` (skip it if there's no extra bring-up, like\n   `neurt`), `rac_static_register_<name>.cpp`, plus your impl files.\n2. **Write the manifest** in `rac_plugin_entry_<name>.cpp`:\n   - `name` = snake_case identity (the library/framework you wrap, or your own\n     codebase unit — NEVER a modality name; see the naming rule in\n     `engines/AGENTS.md`).\n   - `primitives[]` = what you serve; fill the matching vtable slots; leave every\n     other slot explicit NULL (the all-NULL tripwire depends on this).\n   - `runtimes[]` — only devices your compute actually depends on (NULL for\n     cloud/HTTP engines) — see THE RULE in `engines/AGENTS.md`.\n   - `formats[]` = the `RAC_MODEL_FORMAT_ID_*` values you accept (NULL if there's\n     no local model file).\n   - `priority`, `availability` (PUBLIC/PRIVATE), `package_owner`/`package_name`.\n3. **Implement the op-table(s)** honoring the MODEL LIFECYCLE:\n   `create → initialize → use → cleanup → destroy` (see `engines/AGENTS.md` for the\n   exact signatures). Use `RAC_DEFINE_CREATE_ADAPTER(primitive, name)`\n   (`core/include/rac/plugin/rac_plugin_entry.h`) when `create` is a plain forward\n   onto `rac_<primitive>_<name>_create` — sherpa uses it for STT/TTS/VAD. Engines\n   with richer create flows (llamacpp, onnx, neurt, qhexrt) hand-write it.\n4. **`capability_check`** when the engine is platform- or binary-gated: use the\n   shared 3-way helper `rac_engine_unavailable_capability(platform_supported,\n   backend_present)` from `engines/common/rac_engine_unavailable.h`. When the\n   backend can be entirely absent at build time (a private prebuilt archive, a\n   sibling private repo), emit the stub with `RAC_ENGINE_UNAVAILABLE_PLUGIN(name,\n   display, cap_fn)` from the same header — see qhexrt and neurt for the two-mode\n   (ROUTABLE vs STUB) pattern this produces.\n5. **Registration carriers:** `RAC_PLUGIN_ENTRY_DEF(<name>)` returning the vtable; a\n   `rac_backend_<name>_register()`/`_unregister()` pair if you have extra bring-up\n   (called directly by SDK bridges on dynamic-link hosts); a\n   `rac_static_register_<name>.cpp` shim using `RAC_STATIC_REGISTER_BACKEND(<name>)`\n   (routes through the register fn) or `RAC_STATIC_PLUGIN_REGISTER(<name>)` (calls\n   the entry directly) for iOS/WASM static hosts.\n6. **CMake:** call `rac_add_engine_plugin(<name> SOURCES … LINK_LIBRARIES …\n   AVAILABILITY … PACKAGE_OWNER … PACKAGE_NAME …)` (`cmake/plugins.cmake`) from\n   `engines/<name>/CMakeLists.txt`, fronted by your own\n   `option(RAC_BACKEND_<NAME> \"…\" <default>)` + `if(NOT RAC_BACKEND_<NAME>)\n   return() endif()` self-gate (each engine owns its own option — `engines/CMakeLists.txt`\n   just unconditionally `add_subdirectory()`s and lets the child decide). Add\n   `add_subdirectory(<name>)` there. Declare `primitives`/`runtimes`/`formats` ONLY\n   in the C manifest, never in CMake, so the build graph can't drift from what the\n   router routes.\n7. **Android:** if the SDK loads the engine via `System.loadLibrary` +\n   `nativeRegister`, add a JNI bridge with `RAC_DEFINE_ENGINE_JNI_BRIDGE(...)`\n   (standalone per-engine `.so` — onnx, llamacpp) or\n   `RAC_DEFINE_ENGINE_JNI_BRIDGE_NO_ONLOAD(...)` (folded into a host lib that\n   already owns `JNI_OnLoad`, e.g. cloud → `librunanywhere_jni.so`) from\n   `engines/common/rac_engine_jni_bridge.h`. The JVM class-path token in the macro\n   call must match the Kotlin `*Bridge` class byte-for-byte.\n8. **iOS/WASM static hosts:** make sure the host force-loads the plugin —\n   `rac_force_load(<app> PLUGINS <name>)` in the host's CMakeLists — so the\n   static-init Registrar TU survives linker stripping.\n9. **If this engine ships an Electron/npm carrier package**, register it in\n   `scripts/validation/gates/check_plugin_natives.py`'s `REQUIRED_OPS` dict (the\n   ops-table symbol name(s) your entry TU references only on the routable\n   branch) — `package-sdk.sh` calls this automatically, so an unregistered\n   engine's stub/shell builds ship undetected. **Check which FILE actually\n   compiles `rac_plugin_entry_<name>.cpp`** before assuming the carrier\n   (`runanywhere_<name>`) is what to gate: sherpa/onnx/llamacpp compile it into\n   the carrier itself (an undefined symbol reference there is the evidence),\n   but neurt's `rac_add_engine_plugin()` call puts it on the ENGINE target\n   (`rac_backend_neurt`) instead — the carrier there is empty boilerplate with\n   zero symbols. Add the engine id to `EVIDENCE_IN_BACKEND_FILE` if it follows\n   neurt's shape; discovered by actually building neurt for real and running\n   `nm -a` on both files, not by reading the CMakeLists and assuming.\n\n## Procedure B — add a modality to an existing engine\n\nMulti-modality is the whole point of an identity-named engine. `cloud` and `neurt`\nare explicitly built for this and carry the recipe inline as a comment next to\ntheir vtable.\n\n1. Implement the new modality's op-table (e.g. a cloud TTS `g_cloud_tts_ops`,\n   typically backed by a per-modality provider adapter under `providers/`).\n2. **Fill the corresponding slot** in the engine's `rac_engine_vtable_t`\n   (`tts_ops` / `llm_ops` / `embedding_ops` / …) — every other slot must stay\n   explicit NULL.\n3. **Add the primitive** to the manifest's `primitives[]`.\n4. Update `formats[]`/`runtimes[]` only if the new modality actually needs them\n   (per THE RULE — don't declare a runtime your new op-table doesn't depend on).\n\nNo new plugin, no rename, no ABI bump — the engine already owns one\n`rac_engine_vtable_t` with all ten primitive slots; you're just filling another one.\nGrep the target engine's `rac_plugin_entry_<name>.cpp` for the phrase \"To add a\"\n— cloud and neurt both leave a comment naming exactly which slots are still free\nand what to add to `primitives[]`.\n",".agents/skills/sdk-release/SKILL.md":"---\nname: sdk-release\ndescription: Cut a new RunAnywhere SDK release end-to-end in RunanywhereAI/runanywhere-sdks — version bump through a fully green, tagged, drafted-then-published GitHub Release. Stops BEFORE publishing to package registries (npm/Maven/pub.dev/SwiftPM dist) — that is the separate \"sdk-publish\" skill, invoked as the last step here.\n---\n\n# SDK Release (runanywhere-sdks)\n\nRepo: `~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks` (verify with `pwd`/`ls` —\nthis is a workspace subfolder, not the repo root of the whole Qualcomm/ tree, and paths\ndrift; re-check script paths below still exist before trusting them blindly).\n\nThis is a **runbook**, not a description. Follow the steps in order. Every `gh`/`git`\ncommand below is meant to be run close to verbatim — adjust only the version/PR/run-id\nvalues for the release you're actually cutting.\n\n**THIS SKILL HAS BEEN SKIPPED THREE TIMES IN A ROW (v0.20.29, v0.20.30, v0.20.31),\nproducing the IDENTICAL failure each time, DESPITE step 2/3 below documenting the exact\nfix since the v0.20.25/v0.20.29 incidents.** All three times, an agent ran\n`sync-versions.sh`, opened the PR, merged it, and pushed the tag directly — skipping the\n\"dispatch a release candidate BEFORE merging, sync checksums from it, THEN merge\" sequence\nin steps 2-3 — usually because the version bump was one step inside a longer autonomous\nchain (a downstream repo re-pin, a multi-repo release cascade) and felt like \"just a\nversion bump,\" not a release. **A version bump to `core/VERSION` IS a release. If you are\nabout to run `scripts/release/sync-versions.sh` for ANY reason — including re-pinning this\nrepo as a step in some OTHER repo's release — stop and follow this skill from step 1,\nincluding the candidate dispatch. Do not improvise the sequence from memory even if you\ndid it correctly last time; do not skip it because \"this bump is small\" or \"we're in a\nhurry.\"** v0.20.31 additionally caught a second, previously-undetected instance of the\nsame class of bug in the SAME commit: `RunAnywhereMLXRuntime` in the Flutter podspec (which\n`RAC_CHECKSUM_SKIP` exempts from CI verification, so it fails silently for a consumer\ninstead of failing `publish`) was ALSO stale, and had to be independently re-verified\nagainst the same candidate build — see step 3's checksum-location table; check every row\nin it, not just the one CI complained about.\n\nCompanion skill: **sdk-publish** — invoke it at the very end (step 10) for the actual\nnpm / Maven / pub.dev / SwiftPM-distribution publishing. This skill's job stops at a\npublished (non-draft) GitHub Release with correct assets.\n\n---\n\n## 0. Orientation facts (confirm these still hold before relying on them)\n\n- Canonical version file: `core/VERSION` (single line, e.g. `0.20.24`). `core/VERSIONS`\n  also carries a `PROJECT_VERSION=` line that must agree — `sync-versions.sh` updates\n  both, `auto-tag.yml` reads `core/VERSIONS`, `check_release_version_coherence.sh` reads\n  `core/VERSION`. Don't hand-edit either; always go through the script.\n- `scripts/release/sync-versions.sh <version>` — bumps ~20 files (Package.swift,\n  gradle.properties, all package.json/pubspec.yaml, Kotlin/Swift/TS version constants,\n  AGENTS.md). Accepts a leading `v` and strips it.\n- `scripts/validation/gates/check_release_version_coherence.sh` — the gate that enforces\n  \"PR changing VERSION must carry exactly one `release:patch|minor|major` label, and the\n  new version must be exactly base+1 for that bump type.\" Reads `PR_BASE_SHA` and\n  `PR_RELEASE_LABELS_JSON` env vars when run under CI; locally it only checks the\n  canonical-version-format part.\n- `.github/workflows/pr-build.yml` job **`centralization`** is what actually runs the\n  coherence gate in CI (`Release-train version coherence` step), alongside\n  `check_typescript_centralization.sh`, `check_flutter_centralization.sh`,\n  `check_gradle_centralization.sh`, `check_wasm_provenance_contract.sh`,\n  `check_swift_dist_repo_sync.sh`, `check_agents_claude_sync.sh`,\n  `check_no_hardcoded_defaults.sh`. This job's trigger types are\n  `[opened, synchronize, reopened, labeled, unlabeled]` — **adding the release label\n  after opening the PR fires a fresh run**, which is how the gate clears without a new\n  commit (see step 1).\n- `.github/workflows/release.yml` — the build/package/draft-release workflow. Triggers:\n  tag push `v*.*.*` OR `workflow_dispatch` (works on **any branch**, not just tags) with\n  inputs `version` (required), `validate_external_starters` (bool, default false),\n  `publish_from_run_id` (optional), `reuse_native_web_run_id` (optional).\n- `.github/workflows/auto-tag.yml` — fires on `pull_request: closed` to `main` when the\n  merged PR carries a `release:*` label. Verifies the version bump matches the label,\n  pushes the `v<version>` tag, and **explicitly dispatches a fresh `release.yml` run at\n  that tag** (plain `git push` of a tag with `GITHUB_TOKEN` does not auto-trigger\n  workflows, so it calls `gh workflow run release.yml --ref v<version> -f version=<version>`\n  itself). This is the redundant rebuild you must cancel — see step 7.\n- Branch protection on `main`: check\n  `gh api repos/RunanywhereAI/runanywhere-sdks/branches/main/protection` — 2 approvals +\n  code owner review required, but look for a `bypass_pull_request_allowances` (users/teams/apps)\n  list. If your `gh api user --jq .login` is on that list, `gh pr merge --squash --admin`\n  is a legitimate, pre-configured bypass — not a hack. If not on the list, stop and ask a\n  human to approve/merge.\n- Never let any `qhexrt`-named asset reach the public asset set. `release.yml`'s own\n  manifest-assertion step (`find release-flat -maxdepth 1 -type f -iname '*qhexrt*'`)\n  hard-fails the run if one is found — this is enforced, not just policy.\n- **Electron IS part of `release.yml` now** (as of `fix/electron-cicd-staging-url-npu-ane`,\n  2026-08-21) — this reverses the prior \"deliberately separate\" policy noted below by an\n  earlier pass; the repo owner explicitly asked for Electron to produce ready-to-publish\n  artifacts through the real release train like every other SDK. `native_electron`\n  invokes `.github/workflows/electron-native-package.yml` as a **reusable workflow**\n  (`workflow_call`) rather than duplicating its ~400 lines of hard-won native-build logic\n  inline; `sdk_electron` downloads the resulting `electron-packaged-tarballs` artifact,\n  runs `scripts/release/prepublish_check.py` on every tarball, computes checksums, and\n  splits public vs. QHexRT-private before anything reaches `release-flat`. `publish`\n  hard-gates on `sdk_electron` succeeding (same tier as `sdk_kotlin`/`sdk_web`/\n  `sdk_react_native`) but treats `native_electron`'s own result as advisory, because that\n  job's result folds in `native_win_arm64_qhexrt` — a single, personally-owned\n  self-hosted NPU runner whose outage should cost only `@runanywhere/electron-qhexrt`\n  for that release, not the whole SDK train (`native_win_x64_and_package`, the job that\n  actually produces the tarballs, tolerates a failed/skipped QHexRT lane and still packages\n  the other five real artifacts). Like every other SDK here, this still does **not**\n  `npm publish` anything — it only produces the tarballs as GitHub Release assets\n  (public ones) / a workflow artifact (the private QHexRT one) for a human to publish by\n  hand, matching the \"build automated, publish manual\" shape everywhere else.\n  **Electron is no longer WIP on a branch** — the old root `AGENTS.md`/`CLAUDE.md`\n  \"Active issues\" section describing `smonga/electron_upgrade` as in-progress is gone\n  entirely as of this checkout; `bindings/electron/` is a fully merged, real SDK on\n  `main` with its own `AGENTS.md`. Re-check that section is still absent before trusting\n  this note — if it has reappeared, something regressed.\n  See `thoughts/shared/plans/electron_cicd_staging_url_and_npu_ane_pipeline.md` for the\n  full history (this pipeline shipped the placeholder-URL bug three times before\n  `electron-native-package.yml` existed).\n  **QHexRT public/private boundary — CONFIRMED POLICY, not an open flag**: the repo\n  owner explicitly confirmed (2026-08-20) that `@runanywhere/electron-qhexrt` stays\n  public on npm intentionally (it already is, and is a real, working precedent worth\n  studying before touching QHexRT packaging elsewhere) — the QHexRT-stays-private\n  policy enforced everywhere else in this pipeline is specifically about the\n  **GitHub-Release-side** asset set (e.g. Android), a different distribution channel.\n  This is settled; stop re-flagging it as an unresolved contradiction. Only re-raise it\n  if you find evidence the policy has changed.\n\n---\n\n## 0b. NeuRT is a PINNED PREBUILT now — check this before anything else\n\n`native_ios` no longer compiles the private `neurun` repo. It downloads archives that\nrepo publishes, pinned by tag + SHA-256 in `core/VERSIONS`, the same shape as sherpa-onnx:\n\n```\nNEURUN_REPO=RunanywhereAI/neurun      # shared: one repo publishes BOTH engines\nNEURT_RELEASE_TAG=v<version>          # per-engine: a release may carry one lane only\nNEURT_RAC_ABI_VERSION=9\nNEURT_MACOS_ARM64_SHA256=…\nNEURT_IOS_ARM64_SHA256=…\nNEURT_IOS_ARM64_SIMULATOR_SHA256=…\n```\n\n`NEURUN_REPO` is shared because one neurun release carries both engines. The **tags stay\nper-engine** on purpose: that repo can cut a release for one lane only, so a given tag may\ncarry just one engine's assets.\n\n**Run the gate after re-pinning** — it verifies the pins against the release itself, so a\nmistyped or locally-computed value fails there instead of mid-build:\n\n```bash\nNEURUN_TOKEN=\"$(gh auth token)\" bash scripts/validation/gates/check_engine_prebuilt_pins.sh\n```\n\n### Local engine development — there is no source mode\n\nThe engine has no `NEURT_ROOT` path any more; nothing here compiles engine source. To iterate\non NeuRT, build a slice in the neurun checkout and point the engine at it — no release, no\ntag, no pin edit:\n\n```bash\n# in neurun\nNeuRT/tools/scripts/package-rac-dist.sh --sdk-root <this repo> --version 0.0.0-dev \\\n    --slice macos-arm64 --out /tmp/devdist\nmkdir -p /tmp/devslice && tar -xzf /tmp/devdist/neurt-macos-arm64-v0.0.0-dev.tar.gz \\\n    -C /tmp/devslice --strip-components=1\n\n# here\nNEURT_PREBUILT_ROOT=/tmp/devslice cmake -S . -B build/dev -G Ninja \\\n    -DRAC_BUILD_BACKENDS=ON -DRAC_BACKEND_NEURT=ON -DRAC_RUNTIME_COREML=ON\n# expect: Mode: PREBUILT (macos-arm64, ABI 9)\n```\n\n`NEURT_PREBUILT_ROOT` is a **single-slice** root, which is why\n`build-core-xcframework.sh` deliberately ignores it: packaging needs all three slices, so\nhonouring a single-slice override there would let one staged slice satisfy the guard and\nsilently package two non-routable stubs. Use an obviously-not-a-release version like\n`0.0.0-dev` so a dev slice can never be mistaken for a published one.\n\n**Why this changed, and why it matters to you specifically.** `engines/neurt/CMakeLists.txt`\nused to resolve a `NEURT_ROOT` checkout of that private repo and **regex-parse its root\n`CMakeLists.txt`** for a source list. Under that arrangement, commons widened\n`rac_llm_stream_callback_fn` with `tokens_in_delta`, the pinned neurun commit still passed\n4 arguments, and `native_ios` died with *\"too few arguments to function call, expected 5,\nhave 4\"* on **v0.20.15, v0.20.16 and v0.20.17**. Every packaging job needs `native_ios`,\nso all three shipped **iOS-only — 18 assets where a healthy release has 55+**. That is the\nsingle most expensive failure this runbook exists to prevent, and it came from this seam.\n\n**Before opening the release PR**, confirm the pin is current:\n\n```bash\ngrep -A5 '^NEURT_REPO' core/VERSIONS\ngh release view \"$(grep '^NEURT_RELEASE_TAG=' core/VERSIONS | cut -d= -f2)\" \\\n    --repo RunanywhereAI/neurun --json assets --jq '.assets[].name'\n```\n\nExpect six assets — three `neurt-<slice>-v<version>.tar.gz` plus their `.sha256` sidecars.\n\n**If the plugin ABI changed this cycle** (`RAC_PLUGIN_API_VERSION` in\n`core/include/rac/plugin/rac_plugin_entry.h`), the pinned archives are invalid and\n`native_ios` will fail at the download step. Cut a neurun release first — that repo's\n`release_sdk_archives` skill is the runbook — then re-pin both `NEURT_RELEASE_TAG` and\n`NEURT_RAC_ABI_VERSION` here. The downloader refuses to run if `NEURT_RAC_ABI_VERSION`\ndisagrees with the header, so this cannot be half-done.\n\nProve it locally before trusting a tag build:\n\n```bash\nNEURUN_TOKEN=\"$(gh auth token)\" bash scripts/build/download-neurt.sh --all\n```\n\nThat verifies each tarball's SHA-256 **and** that its `RECEIPT.json` names the same ABI\nversion this repo defines. Then check configure prints `Mode: PREBUILT (<slice>, ABI N)`.\n`SOURCE` means a local neurun checkout was picked up (prebuilt is meant to win, so the\ndownload did not land); `SHELL` means the engine is **not routable** and a release built\nnow would ship an engine that registers, claims Apple availability and serves nothing.\n\n`release.yml` treats the downloader's exit **3** (\"no token — degrade to the shell\") as\n**fatal**. That degrade is right for a fork PR and catastrophic for a release.\n\n### The complete engine matrix — who consumes what, and how\n\nThere are only **three fetch points in the entire repo**. Everything else consumes an\nsdks artifact, which is why a single fetch fix can repair five SDKs at once — and why a\nsingle missing fetch can silently break several.\n\n```\nneurun release (private)\n  ├─ native_ios         ──NeuRT x3 slices──►  RABackendNeuRT.xcframework\n  │                                              ├─ Swift  (Package.swift binary target)\n  │                                              ├─ Flutter / React Native (co-vendored\n  │                                              │   in the onnx package's podspec)\n  │                                              └─ rcli-macos (stages it, then GREPS\n  │                                                  `backends --json` for neurt)\n  ├─ native_android     ──QHexRT arm64-v8a──►  one build stages jniLibs into\n  │                                              Kotlin + React Native + Flutter\n  └─ electron           ──NeuRT macos-arm64 AND QHexRT win-arm64──►  its own natives\n```\n\n| target | NeuRT | QHexRT | note |\n|---|---|---|---|\n| Swift (iOS/macOS) | ✅ xcframework | — | NeuRT is Apple-only |\n| Flutter / React Native (Apple) | ✅ xcframework | — | co-vendored in the onnx package |\n| Flutter / React Native (Android) | — | ✅ jniLibs | from the same Android build as Kotlin |\n| Kotlin (Android) | — | ✅ jniLibs | arm64-v8a only; the only ABI with a Hexagon NPU |\n| Electron macOS | ✅ fetches | — | builds its own natives |\n| Electron win-arm64 | — | ✅ fetches | |\n| rcli macOS | ✅ xcframework | — | **best check in the tree** (below) |\n| rcli linux / windows-x64 | — | — | correct: NeuRT is Apple-only, QHexRT is win-arm64 |\n\n**`rcli-macos` is the strongest verification that exists here.** It greps\n`backends --json` for `\"name\":\"neurt\"` — and a non-routable shell *refuses\nregistration*, so that grep proves the engine is **routable**, not merely present. Copy\nthis pattern when adding an engine rather than checking for a file on disk.\n\n**Two silent-failure traps this matrix exists to prevent:**\n\n- **`RAC_BACKEND_<X>=ON` does not mean the engine works.** Without a payload the engine\n  builds as a non-routable shell and registration is refused — no build error. Android\n  shipped that way for months, and `rcli-macos-release` still sets\n  `RAC_BACKEND_NEURT: ON` regardless of whether a payload was fetched.\n- **`copy_if_exists` in `build-core-android.sh` silently no-ops** when a lib was never\n  built. That is the same guarded-copy pattern that shipped Windows with zero engines for\n  several releases. The release now asserts `librunanywhere_qhexrt.so` is really inside\n  the arm64-v8a archive.\n\n`scripts/validation/gates/check_engine_prebuilt_pins.sh` asserts all of the above plus\nthe pins, the ABI, header drift against the receipt, and (with a token) the release\ncontents. Run it before cutting a release; it is also wired into `pr-build` offline.\n\n### QHexRT is pinned the same way — check it too\n\nThe Hexagon-NPU engine crosses the identical boundary, published by the same neurun release:\n\n```\nQHEXRT_RELEASE_TAG=v<version>\nQHEXRT_ARM64_V8A_SHA256=…      QHEXRT_ARM64_V8A_RECEIPT=…\nQHEXRT_WIN_ARM64_SHA256=…      QHEXRT_WIN_ARM64_RECEIPT=…\n```\n\nTwo pins per ABI, not one: the tarball checksum **and** the payload's own build-receipt\nhash. The receipt is the directory name under `engines/qhexrt/prebuilt/versions/`, so\npinning it is what makes the pin describe the bytes rather than trusting the archive's\nown claim about itself.\n\n```bash\nNEURUN_TOKEN=\"$(gh auth token)\" bash scripts/build/download-qhexrt.sh --abi arm64-v8a\nNEURUN_TOKEN=\"$(gh auth token)\" bash scripts/build/download-qhexrt.sh --abi win-arm64\n```\n\n**`current` selects ONE ABI at a time.** Both payloads can sit in `versions/` together, but\na build targets one platform — so re-run the downloader when switching between an Android\nand a Windows build. The downloader ends by running `validate-qhexrt-prebuilt.py` itself,\nso a bad payload fails at the pin rather than mid-build.\n\n**The QHexRT lane in neurun is deliberately non-blocking.** It runs on a single\nself-hosted Snapdragon box (QAIRT is licence-gated and exists on no hosted runner). If\nthat machine is offline, neurun's release publishes the Apple artifacts and **warns** that\nno QHexRT payload was produced — and the SDK simply keeps its previously pinned prebuilt.\nThat is correct behaviour, not a failure: do not block an SDK release on it. Only treat it\nas blocking if this release is specifically meant to ship new NPU kernels.\n\n**Do not hand-stage `engines/qhexrt/prebuilt/`.** That was the old flow, and it is how the\nElectron win-arm64 lane once pinned a receipt staged months earlier and silently shipped\nNPU kernels from an old commit. If the tree looks stale, re-pin and re-download; never\ncopy a payload in by hand.\n\n---\n\n## 1. Decide the version and open the release PR\n\nCheck for an already-open release PR first — don't duplicate work:\n\n```bash\ngh pr list --repo RunanywhereAI/runanywhere-sdks --label release:patch --label release:minor\n```\n\n(gh ORs multiple `--label` flags into a single AND-filtered query per invocation is not\nguaranteed across all gh versions — if unsure, run the two label queries separately:\n`--label release:patch` then `--label release:minor`, plus `--label release:major`.)\n\nPick a patch/minor/major bump based on the changes since the last tag. Then:\n\n```bash\ngit checkout -b release/v<version>\n./scripts/release/sync-versions.sh <version>      # e.g. 0.20.25 (no 'v' needed, either works)\nbash scripts/validation/gates/check_release_version_coherence.sh\n```\n\nThe coherence script may point out generated locks/changelogs that need regenerating\n(e.g. package-lock.json version bumps it expects but didn't find, or a missing CHANGELOG\nheading for the new version). Fix everything it flags, and add **explicit, human-written\nchangelog entries** — the gate requires a heading for the new version to exist; it does\nnot write prose for you.\n\nOpen the PR and **apply exactly one `release:patch` or `release:minor` (or `release:major`)\nlabel** — this is required, not optional. Two failure modes to know:\n\n- If you forget the label and push a commit later, the `centralization` job's\n  `Release-train version coherence` step fails.\n- **Adding the label after the PR is already open does NOT get picked up by the run that\n  already executed.** Because `pr-build.yml`'s `centralization` job triggers on\n  `labeled`/`unlabeled` PR events, applying the label fires a **new** run of the whole\n  job — that new run is what goes green. Don't push an empty commit to \"retrigger\" it;\n  just add/re-add the label (or use `gh pr edit <pr> --add-label release:patch`, and if it\n  was already present, remove + re-add to force the event).\n\n---\n\n## 2. Get REAL native-artifact checksums BEFORE merging — dispatch a release candidate\n\nDo not merge on faith that native builds will succeed. `release.yml`'s\n`workflow_dispatch` trigger works on **any branch**, including your unmerged release\nbranch — use it to build a full release candidate first:\n\n```bash\ngh workflow run release.yml --ref release/v<version> -f version=<version>\n```\n\nGet the run id and watch it:\n\n```bash\ngh run list --repo RunanywhereAI/runanywhere-sdks --workflow=release.yml --limit 5\n```\n\n**Do not block synchronously waiting on it.** Load the `Monitor` tool (or a background\npoll loop) to watch for completion/failure, and keep doing other release prep (changelog\nreview, CodeRabbit thread triage, branch-protection check) in the meantime. Keep\nnotifications to real state changes only — don't ping on every intermediate job\ncompleting, only on the run's overall failure or full completion.\n\n### REAL job durations — measured, not guessed. NEVER cancel a job for being \"slow\".\n\nMeasured on the 0.20.25 train (2026-08-21/22). A release train legitimately takes\n**4-5 hours** wall-clock when `native_web` has to build.\n\n| Job | Real duration | Notes |\n|---|---|---|\n| **`native_web`** (release.yml) | **~3h51m** | The long pole, by far. Its `Vendor ONNX Runtime and Sherpa-ONNX WASM archives` step alone is **~71 minutes** — it BUILDS onnxruntime + sherpa to WASM from source; it is not a git clone. Then 4 more WASM builds: CPU core+llamacpp+onnx ~89m, WebGPU llamacpp ~37m, WebGPU onnx+sherpa ~33m. |\n| `native_ios` | ~60-75 min | XCFrameworks + MLX. No longer compiles neurun — it downloads the pinned NeuRT archives (see §0b), which is seconds, not minutes. |\n| `swift-spm` (pr-build) | **69-85 min** | `Build XCFramework` step is most of it. Measured across 6 consecutive `main` runs (2026-08-21/22): 69, 78, 75, 85, 77, 69. A run sitting at ~78 minutes is **healthy**, not hung — check this row before reaching for cancel. |\n| `kotlin-android` (pr-build) | ~40 min | |\n| `python-windows` (pr-build) | ~40 min | |\n| `electron (windows-2022)` (electron-sdk-ci) | ~40 min | |\n| `rcli-windows` | ~30 min | |\n| `native_electron / native_win_arm64_qhexrt` | ~28 min | Self-hosted; may sit **queued** for a long time first if the single runner is busy. Queued ≠ hung. |\n| `native_electron / native_win_x64_and_package` | ~35-45 min | |\n| `wasm` (pr-build) | ~12 min | **Do NOT use this as a proxy for `native_web`.** Totally different scope. |\n\n**The mistake this table exists to prevent**: `pr-build.yml`'s `wasm` job takes ~12\nminutes, so a ~50-minute `native_web` \"vendor\" step looks hung. It is not. On the 0.20.25\ntrain this misread caused two healthy `native_web` jobs to be cancelled at 2h19m and\n~55m, wasting ~3 hours and two full candidate builds. Before concluding \"hung\", check\nstep-level progress and compare against THIS table:\n\n```bash\ngh run view <run-id> --repo RunanywhereAI/runanywhere-sdks --json jobs \\\n    --jq '.jobs[] | select(.name==\"native_web\") | {startedAt, steps: [.steps[] | select(.status==\"in_progress\") | .name]}'\n```\n\n### `reuse_native_web_run_id` is the correct way to avoid re-paying that ~4 hours\n\nIf a run needs redoing and **nothing under `bindings/web/wasm/`, `engines/`, or `core/`\nchanged** since a run whose `native_web` succeeded, reuse it instead of rebuilding:\n\n```bash\ngit diff --stat <good-run-sha> <current-sha>      # prove the scope first\ngh workflow run release.yml --ref release/<version> \\\n    -f version=<version> -f reuse_native_web_run_id=<good-run-id>\n```\n\n`native_web` then shows `skipped`, and `sdk_web` + `validate_consumer_web` still run and\npass against the reused artifact — which is also your proof the reuse was valid. Track the\nrun id that **originally built** it: that is the id you keep citing (see the chaining note\nin step 7).\n\n---\n\n## 3. Sync checksums once native_ios finishes\n\nPre-tag, `gh release download` will not work (there is no release yet) — pull the run's\nartifact directly:\n\n```bash\ngh run download <candidate-run-id> --repo RunanywhereAI/runanywhere-sdks \\\n    --name native-ios-macos --dir release-artifacts/native-ios-macos\n```\n\nVerify every `.zip` against its `.sha256` sidecar **yourself** before trusting it:\n\n```bash\ncd release-artifacts/native-ios-macos\nfor f in *.zip; do shasum -a 256 -c \"${f}.sha256\" || echo \"MISMATCH: $f\"; done\ncd -\n```\n\nThen sync:\n\n```bash\nbindings/swift/scripts/sync-checksums.sh release-artifacts/native-ios-macos\n```\n\n**KNOWN FACT — do not treat this as a bug**: `RunAnywhereMLXRuntime` (and occasionally\n`RACommons`) are **not byte-reproducible** between rebuilds of identical source. Expect\ntheir checksums to differ from any prior candidate build every single time you rebuild,\neven with zero source changes. Only investigate if a checksum for a framework that\nhistorically IS reproducible (e.g. plain XCFrameworks with no Swift codegen step)\nsuddenly changes.\n\n**Where each checksum actually lives — these are NOT all in one file:**\n\n| Manifest | Carries |\n|---|---|\n| root `Package.swift` | `RACommons` + the 5 backend XCFrameworks (6 checksums total) |\n| nested Flutter `Package.swift` (under `bindings/flutter/`) | same 6 |\n| `bindings/flutter/packages/runanywhere_mlx/ios/runanywhere_mlx.podspec` → `mlx_checksums` hash | `RunAnywhereMLXRuntime`, `RunAnywhereMLXMetal`, `RunAnywhereMLXResources` — **only here**, nowhere else |\n\nAfter `sync-checksums.sh` runs, verify centralization didn't drift and re-run the version\ncoherence gate locally:\n\n```bash\nbash scripts/validation/gates/check_flutter_centralization.sh\nbash scripts/validation/gates/check_release_version_coherence.sh\n```\n\nCommit and push the checksum-sync commit to the release branch:\n\n```bash\ngit add Package.swift bindings/flutter/Package.swift \\\n    bindings/flutter/packages/runanywhere_mlx/ios/runanywhere_mlx.podspec\ngit commit -m \"release: sync v<version> checksums from candidate run <candidate-run-id>\"\ngit push\n```\n\n### THIS STEP IS NOT OPTIONAL — skipping it fails `publish` after the tag already exists\n\n**What happens if you skip it** (observed on the 0.20.25 train): every build and package\njob goes green, `publish` gets all the way past flatten and the asset-manifest assertion,\nthen dies at **`Verify built Apple checksums match tagged package contracts`**:\n\n```\nmismatch: RACommonsBinary\n  Package.swift: 0609dae6...      release zip: 1ae55ae4...\nmismatch: RABackendLlamaCPPBinary\n  Package.swift: 0597442f...      release zip: 7062f7c8...\n>> Done. 8 processed, 0 missing, 4 failed.\nERROR: built Swift archives do not match the immutable tagged manifest\n```\n\nNote `8 processed / 4 failed` is really **2 distinct archives counted twice** (root\nmanifest + Flutter manifest each). On that train `RACommons` and `RABackendLLAMACPP`\ndrifted while ONNX/Sherpa/NeuRT/MLX/MLXMetal/MLXResources all matched — so \"6 of 8 passed\"\nis a normal-looking partial failure, not evidence of something exotic.\n\n**Recovery once the tag already exists** (this is safe and was done on 0.20.25):\n\n1. Confirm nothing was actually published — a failed `publish` dies *before* creating the\n   release, so there is usually nothing to undo:\n   ```bash\n   gh release view v<version> --repo RunanywhereAI/runanywhere-sdks   # expect \"release not found\"\n   ```\n2. Download the **exact artifacts the release will publish** (the candidate run named in\n   `publish_from_run_id`), verify each against its sidecar, and sync on a branch off `main`.\n3. Merge that branch, then **re-point the tag** at the new commit. `git push origin --delete`\n   can hang on flaky networks; the API is more reliable:\n   ```bash\n   gh api -X DELETE repos/RunanywhereAI/runanywhere-sdks/git/refs/tags/v<version>\n   gh api -X POST   repos/RunanywhereAI/runanywhere-sdks/git/refs \\\n       -f ref=\"refs/tags/v<version>\" -f sha=\"<new-main-sha>\"\n   ```\n   Only do this while **no release exists and nothing has been published**. If a release\n   was already published, do NOT move the tag — cut the next patch version instead.\n4. **Creating a tag via the API DOES trigger `release.yml`'s `push` trigger** (unlike a\n   `GITHUB_TOKEN` git-push of a tag, which does not). So immediately after re-tagging you\n   will have TWO runs: a full rebuild from the `push` event, and your own\n   `workflow_dispatch`. Identify by event and cancel the `push` one:\n   ```bash\n   gh run list --repo RunanywhereAI/runanywhere-sdks --workflow=release.yml --limit 3 \\\n       --json databaseId,event,headBranch --jq '.[] | \"\\(.databaseId) \\(.event) \\(.headBranch)\"'\n   gh api -X POST repos/RunanywhereAI/runanywhere-sdks/actions/runs/<push-run-id>/cancel\n   ```\n\nCheapest prevention: run `sync-checksums.sh --check <zip_dir>` against the candidate's\nartifacts and require **`N processed, 0 missing, 0 failed`** before you merge the release\nPR at all.\n\n**Recurred on v0.20.30 and again on v0.20.31** — same `mismatch: RACommonsBinary` /\n`mismatch: RACommons` failure, same root cause (step 2's candidate dispatch skipped\nentirely both times), fixed the same way both times: compute the real checksum from the\nalready-built release-artifact zip (`gh run download <failed-run-id> --name\nnative-ios-macos`), commit it on a fix branch, merge, then move the tag (§10d) to\nre-trigger `release.yml`. On v0.20.31 this ALSO surfaced the `RunAnywhereMLXRuntime`\npodspec entry as independently stale — `RAC_CHECKSUM_SKIP` means `publish` never checks\nit, so it would have shipped broken to Flutter consumers silently. **When fixing this\nfailure after the fact, check every row of the checksum-location table in step 3, not\nonly the archive named in the CI error** — the ones `RAC_CHECKSUM_SKIP` exempts are\nexactly the ones that won't tell you.\n\n---\n\n## 4. Watch for the \"package intentionally doesn't bundle a sibling dep\" bug class\n\nThis exact release hit a real bug in `bindings/web/scripts/package-sdk.sh`:\n`@runanywhere/web-onnx` deliberately does **not** bundle its own `proto-ts` copy (only\n`core`/`llamacpp` do — enforced by `bindings/web/scripts/validate_public_packages.py` and\nmirrored in `bindings/react-native/scripts/validate_public_packages.py`), but the\npackaging script's install-smoke-test (`verify_candidate_install`, around line 165 of\n`package-sdk.sh`) never supplied a resolution target for that unbundled dependency, so\n`npm install` reached out to the real npm registry for a not-yet-published version and\nfailed with `ETARGET`.\n\nFix pattern (already applied, keep it this way): the `onnx` call to\n`verify_candidate_install` passes the just-built local `proto-ts` tarball\n(`$PROTO_ARCHIVE`, built via `npm pack ../proto-ts`) as an explicit extra install arg —\nsee lines ~209-233 of `bindings/web/scripts/package-sdk.sh`, where `core` and `llamacpp`\ncalls omit it (they bundle proto-ts themselves) but `onnx`'s call appends `\"$PROTO_ARCHIVE\"`.\n\n**Generalize this as a standing check for every release**: grep for the asymmetric\nbundling flags before trusting any packaging script —\n\n```bash\ngrep -n \"expects_bundled_proto\\|--bundle\" \\\n    bindings/web/scripts/package-sdk.sh bindings/react-native/scripts/package-sdk.sh\n```\n\nWhenever a package intentionally does NOT bundle a sibling first-party dependency, its\nlocal pre-publish verification step must supply that dependency some other way (a local\ntarball) — the real registry version genuinely does not exist yet at candidate-build\ntime, and letting `npm`/`yarn` hit the real registry will `ETARGET`.\n\n**Before trusting any packaging script is bug-free, actually run it** against the real\ndownloaded native artifacts and check the exit code — do not just read the code and\nassume it's fine:\n\n```bash\ncd bindings/web && ./scripts/package-sdk.sh   # then check $?\ncd bindings/react-native && ./scripts/package-sdk.sh\n```\n\n### 4b. The two Electron-specific release bugs (both fixed — know the shapes)\n\nThe 0.20.25 train was the **first** release to run Electron packaging inside `release.yml`,\nand it surfaced two defects that no local test or green PR check could have caught. Both\nare fixed; recognize the shapes because they generalize.\n\n**(1) `sync-versions.sh` has a hardcoded package list, and new packages get missed.**\n`bindings/electron/packages/neurt/package.json` sat at the previous version while every\nsibling moved, because `neurt` was added (electron#734) after that list was last touched.\nSymptom: `native_win_x64_and_package` fails with\n\n```\nERROR: npm pack did not produce .../runanywhere-electron-neurt-<version>.tgz\n```\n\n— because `npm pack` correctly produced `...-neurt-<OLD-version>.tgz` from the stale\nmanifest. **Whenever you add a package anywhere, add it to `sync-versions.sh` the same\nday.** Cheap pre-flight check that catches the whole class:\n\n```bash\ngrep -h '\"version\"' bindings/electron/package.json bindings/electron/packages/*/package.json \\\n    bindings/electron/native/package.json | sort -u     # expect exactly ONE distinct version\n```\n\n**(2) `publish` downloads EVERY artifact, including intermediate hand-off bundles.**\n`publish`'s `Download all artifacts` step has no name filter. `electron-packaged-tarballs`\nis `native_win_x64_and_package`'s raw output whose only purpose is feeding `sdk_electron`,\nwhich re-uploads the curated public set as `sdk-electron`. Flattening both duplicated all\n5 Electron tarballs *and* dragged in the raw bundle's own `proto-ts` copy, which collided\nwith `sdk_web`'s:\n\n```\n::error::flatten collision: release-flat/runanywhere-proto-ts-<v>.tgz already exists\n::error::Refusing to publish — 6 filename collision(s) detected during flatten\n```\n\nFixed by pruning that directory in the flatten `find`\n(`-type d -name \"electron-packaged-tarballs\" -prune -o -type f \\( ... \\)`). **Standing\nrule: any new intermediate artifact must be either excluded from the flatten or named so\nit cannot collide.** When editing that `find`, test the exact expression against a fake\n`release-artifacts/` tree locally first — `-prune -o` + `-print0` is easy to get subtly\nwrong and the only other way to find out costs a full release run.\n\n---\n\n## 5. Resolve CodeRabbit (or any bot) review findings properly\n\nDo not resolve a review thread without a real, evidence-backed reply. Steps:\n\n1. Fetch unresolved threads via GraphQL:\n\n```bash\ngh api graphql -f query='\nquery($owner:String!, $repo:String!, $pr:Int!) {\n  repository(owner:$owner, name:$repo) {\n    pullRequest(number:$pr) {\n      reviewThreads(first:100) {\n        nodes { id isResolved comments(first:10) { nodes { id databaseId body } } }\n      }\n    }\n  }\n}' -f owner=RunanywhereAI -f repo=runanywhere-sdks -F pr=<pr-number>\n```\n\n2. For each unresolved thread, reply via REST with `in_reply_to` set to the comment's\n   `databaseId` (not the GraphQL node id), citing concrete evidence — a real run id,\n   a real checksum, a real commit SHA, not \"looks fine\":\n\n```bash\ngh api -X POST repos/RunanywhereAI/runanywhere-sdks/pulls/<pr-number>/comments \\\n    -f body=\"Verified against candidate run <run-id>: RunAnywhereMLXRuntime checksum\nmatches release-artifacts/native-ios-macos/RunAnywhereMLXRuntime-ios-v<version>.zip.sha256.\nSee commit <sha>.\" \\\n    -F in_reply_to=<comment-database-id>\n```\n\n3. Only then resolve, via GraphQL mutation on the thread id from step 1:\n\n```bash\ngh api graphql -f query='\nmutation($threadId:ID!) { resolveReviewThread(input:{threadId:$threadId}) { thread { isResolved } } }\n' -f threadId=<thread-node-id>\n```\n\n---\n\n## 6. Merge\n\nBefore merging, verify:\n\n```bash\ngh pr view <pr-number> --repo RunanywhereAI/runanywhere-sdks --json mergeable,reviewDecision\n```\n\nZero unresolved review threads (re-check the GraphQL query from step 5 — it should return\nnone with `isResolved: false`), and `mergeable == \"MERGEABLE\"`.\n\nConfirm bypass eligibility before relying on `--admin`:\n\n```bash\ngh api repos/RunanywhereAI/runanywhere-sdks/branches/main/protection \\\n    --jq '.required_pull_request_reviews.bypass_pull_request_allowances'\ngh api user --jq .login\n```\n\nIf your login appears in that allowance list (users, or a team/app you belong to):\n\n```bash\ngh pr merge <pr-number> --repo RunanywhereAI/runanywhere-sdks --squash --admin\n```\n\nIf not on the list, **stop** — ask a human to approve or merge instead of forcing it.\n`--admin` is a legitimate configured bypass only for allow-listed identities; do not use\nit to route around review for anyone else.\n\n---\n\n## 7. GOTCHA — cancel the auto-tag-dispatched rebuild, dispatch a publish-only run instead\n\nThe moment the PR merges, `auto-tag.yml` runs, pushes the `v<version>` tag, and **itself**\ndispatches a brand-new full `release.yml` run at that tag (see orientation section — plain\ntag pushes via `GITHUB_TOKEN` don't auto-trigger, so the workflow does it explicitly).\nThis is redundant — you already have a fully green, checksum-verified candidate from step\n2-3 — and wasteful, because Apple/MLX bytes will drift again for zero benefit (see the\nnon-reproducibility fact in step 3).\n\n**Cancel it immediately, before it burns real compute** (ideally within the first minute\nor two, before native_ios starts real work):\n\n```bash\ngh run list --repo RunanywhereAI/runanywhere-sdks --workflow=release.yml --limit 5\ngh api -X POST repos/RunanywhereAI/runanywhere-sdks/actions/runs/<auto-triggered-run-id>/cancel\n```\n\nThen dispatch a **publish-only** run that reuses your already-verified candidate's exact\nbytes instead of rebuilding:\n\n```bash\ngh workflow run release.yml --ref v<version> \\\n    -f version=<version> \\\n    -f publish_from_run_id=<candidate-run-id>\n```\n\n**CRITICAL SUB-GOTCHA**: if your candidate run in step 2 itself passed\n`reuse_native_web_run_id=<X>` to skip rebuilding WASM (a packaging-only re-release), that\ncandidate run **never uploaded its own copy of the `native-web` artifact** — it was\nskipped. `publish_from_run_id` alone will then fail during the release-manifest assertion\nwith `\"Missing or empty Web WASM payload\"`, because the publish job's artifact-download\nstep only pulls `reuse_native_web_run_id`'s artifact when that input is passed to *this*\ndispatch too (see `.github/workflows/release.yml` around the \"Download reused Web natives\"\nstep — it is gated on `inputs.reuse_native_web_run_id != ''`, independent of\n`publish_from_run_id`). You must **also** pass the same original run id:\n\n```bash\ngh workflow run release.yml --ref v<version> \\\n    -f version=<version> \\\n    -f publish_from_run_id=<candidate-run-id> \\\n    -f reuse_native_web_run_id=<X>\n```\n\nThis chains forward across releases: whichever run **originally** built `native_web` is\nthe run you keep citing at every subsequent `reuse_native_web_run_id`, not the most recent\nrun that merely borrowed it from an earlier one. Track that original run id explicitly if\nyou're doing back-to-back packaging-only releases.\n\n---\n\n## 8. Verify the draft release's asset set\n\nThe publish job only creates a **draft** GitHub Release (`softprops/action-gh-release`,\n`draft: true`) with built assets attached — it does not touch npm/Maven/pub.dev at all\n(grep `release.yml` for `npm publish`, `mavenCentral`, `pub publish` — none exist there).\n\nExpected asset count for a full, non-Windows-focused release: **~65 assets** —\n\n- 9 Apple archives (iOS/macOS XCFrameworks: RACommons + 5 backends + MLX runtime/Metal/\n  resources + CoreML/NeuRT)\n- 3 Android RACommons zips (arm64-v8a, armeabi-v7a, x86_64)\n- 1 Linux RACommons tar.gz\n- 1 Windows RACommons zip (preserved as an asset, advisory — see step 9)\n- 1 Web WASM tar.gz (`RACommons-web-v<version>.tar.gz`)\n- 3 rcli tarballs (macOS arm64, Linux x86_64, Windows x86_64 — macOS may also have a\n  notarized `.dmg` if Developer ID/notary secrets are configured; this is soft-skipped\n  otherwise, not a failure)\n- 1 Kotlin Maven repo zip\n- 8 npm tarballs (proto-ts, web core, web-llamacpp, web-onnx, RN core, RN llamacpp, RN mlx,\n  RN onnx)\n- 5 Electron npm tarballs (`runanywhere-electron-<v>.tgz` core, `-llamacpp`, `-onnx`,\n  `-sherpa`, `-neurt`) — added by `sdk_electron`; each has its own `.sha256` sidecar\n  counted separately below. **`electron-qhexrt` is deliberately excluded** — it still\n  gets built and packaged, but only as the `sdk-electron-qhexrt-private` workflow\n  artifact (not a release asset); download it separately for a manual `npm publish`.\n- 1 `MANIFEST.txt`\n- **explicitly ZERO** `qhexrt`-named files (the manifest assertion step hard-fails the run\n  if it finds any — confirm the run actually reached the publish job and didn't die there)\n\nEvery asset above except `MANIFEST.txt` has a `.sha256` sidecar, which is itself a\nseparate asset. **v0.20.25 landed at exactly 65 assets** — use that as the concrete\nbaseline rather than re-deriving the arithmetic.\n\nCross-check every checksum's `.sha256` sidecar against what got committed to\n`Package.swift` / the flutter `Package.swift` / the MLX podspec in step 3 — they must\nmatch byte for byte, because this run's artifacts are exactly what step 10 (sdk-publish)\nships to real consumers.\n\n```bash\ngh run view <publish-run-id> --repo RunanywhereAI/runanywhere-sdks --json jobs \\\n    --jq '.jobs[] | {name, conclusion}'\n\n# Asset count + the Electron set + the qhexrt-leak assertion, in three commands:\ngh release view v<version> --repo RunanywhereAI/runanywhere-sdks --json assets --jq '.assets|length'\ngh release view v<version> --repo RunanywhereAI/runanywhere-sdks --json assets \\\n    --jq '[.assets[].name | select(startswith(\"runanywhere-electron\"))] | sort'\ngh release view v<version> --repo RunanywhereAI/runanywhere-sdks --json assets \\\n    --jq '[.assets[].name | select(test(\"qhexrt\";\"i\"))]'          # MUST be []\n```\n\n### Prove the artifacts are real — don't trust green checkmarks\n\nA green `publish` only proves the assertions ran. To prove the bytes are right, run the\nrepo's own fail-closed gate against the **published** tarballs. This is the single highest\n-value verification in the whole runbook and it takes about a minute:\n\n```bash\ngh release download v<version> --repo RunanywhereAI/runanywhere-sdks \\\n    -p 'runanywhere-electron*.tgz' -D /tmp/verify\npython3 scripts/validation/gates/check_plugin_natives.py --expect-version <version> \\\n    $(for t in /tmp/verify/*.tgz; do printf -- '--tarball %s ' \"$t\"; done)\n```\n\nExpect `All N check(s) passed: plugin natives are routable and no staging-URL placeholder\nwas found.` On v0.20.25 that was **20 checks** across 5 tarballs, asserting three separate\nthings per binary: the real `STAGING_BASE_URL` is baked in (no `YOUR_STAGING_BASE_URL`\nplaceholder), `rac_commons` embeds the right version string, and each backend is actually\n**routable** (real op tables — `g_llamacpp_ops`, `g_sherpa_stt_ops`, `g_neurt_llm_ops`,\netc.), which is what distinguishes a real plugin from a same-sized shell.\n\nSanity-check the shipped layout too — a tarball missing a platform is a silent regression:\n\n| Package | Expected `prebuilds/` |\n|---|---|\n| `@runanywhere/electron` | `darwin-arm64` + `win32-x64` + `win32-arm64` |\n| `-llamacpp`, `-onnx`, `-sherpa` | `darwin-arm64` + `win32-x64` |\n| `-neurt` | `darwin-arm64` only (Apple Neural Engine) |\n| `-qhexrt` (private artifact) | `win32-arm64` only (Hexagon NPU) |\n\n---\n\n## 9. Windows handling — build/preserve only, never auto-publish\n\n`native_windows` and `native_rcli_windows` run and their outputs are **preserved** as\nrelease assets (proving the binaries build), but nothing Windows-specific is published to\nany package registry as part of the standard flow unless a human explicitly asks for it —\nthat's a deliberate, separate decision, not an oversight.\n\nFor anything that needs actual execution on Windows (smoke-testing the native Windows\nbuild, or Electron's Windows packaging), do not assume a macOS cross-build proves\nWindows correctness — SSH to the real Windows box. The team keeps a **Windows ARM64\nmachine reachable over SSH**; its hostname, user, key, and mesh-VPN node name are\noperator-specific and deliberately not recorded in this repo — read the Windows/ARM64\nhost entry out of the operator's `~/.ssh/config` and use that alias. If there is no such\nentry, the box is not configured for you: say so and ask, rather than guessing.\n\n```bash\nWIN_BOX=<the ssh alias from ~/.ssh/config>\n# Ask the mesh-VPN client whether the node is up FIRST (e.g. `tailscale status`,\n# matching on that host entry's HostName) — never just try SSH and retry on timeout.\nssh -o ConnectTimeout=8 \"$WIN_BOX\" \"whoami\"\n```\n\nIf the VPN reports the node offline/asleep, **that's not a config problem** — ask the\nuser to wake the box; don't debug SSH config against a machine that's simply off.\n\n---\n\n## 10. Publish the draft, then hand off\n\nOnce the draft's asset set is verified complete and correct (step 8):\n\n```bash\ngh release edit v<version> --repo RunanywhereAI/runanywhere-sdks --draft=false --latest\n```\n\nThis is the point where SwiftPM's `from: \"<version>\"` resolution and the download URLs\nbaked into `Package.swift` start actually working for real external consumers — draft\nrelease assets are not publicly downloadable, so nothing before this line is\nexternally live.\n\n**Hand off to the `sdk-publish` skill** for the actual npm / Maven Central / pub.dev /\nSwiftPM-distribution-repo publishing steps. This skill's job ends here.\n\n---\n\n## 10b. MANDATORY, and it turns `main` RED until you do it — cut runanywhere-swift\n\nThe moment `v<version>` exists in this repo, `check_swift_dist_repo_sync.sh` (run by\n`pr-build.yml`'s `centralization` job) **fails on every PR and every push to `main`**:\n\n```\n[FAIL] runanywhere-swift has no 0.20.25 tag (latest: 0.20.24)\n       v0.20.25 is published here, so 'from: \"0.20.25\"' is broken for every\n       SwiftPM consumer of https://github.com/RunanywhereAI/runanywhere-swift.git\n```\n\nThis is **by design**, not a bug, and it is the #1 reason a release looks \"not green\"\nafter everything else succeeded. Do not go hunting for a code defect — cut the repo:\n\n```bash\ngit clone https://github.com/RunanywhereAI/runanywhere-swift.git /tmp/ra-swift\nbindings/swift/scripts/sync-dist-repo.sh --zips <zip_dir> --tag /tmp/ra-swift\n# verify BEFORE pushing (content check passes locally; the tag check needs the push)\nRUNANYWHERE_SWIFT_DIST_REPO=/tmp/ra-swift \\\n    bash scripts/validation/gates/check_swift_dist_repo_sync.sh\ngit -C /tmp/ra-swift push origin main --follow-tags\nbash scripts/validation/gates/check_swift_dist_repo_sync.sh    # now [OK]\n```\n\nGotchas found doing this on 0.20.25:\n\n- `--check` **cannot be combined** with `--zips`/`--commit`/`--tag` (hard error). Run a\n  bare `--check <repo>` first to preview, then the real invocation separately.\n- `--check` legitimately reports `ERROR: distribution repo is out of sync` before you\n  sync — that's the preview telling you what it will change (expect the `sdkVersion` bump,\n  ~3 changed files, `0 added / 0 removed`). Non-zero exit there is not a failure.\n- The script **never pushes**; `--tag` only creates the tag locally. The dist repo tag has\n  **no `v` prefix** (`0.20.25`, not `v0.20.25`) while the monorepo tag does.\n- Running the gate locally right after `--tag` still shows `[FAIL] ... has no <version>\n  tag` while simultaneously showing `[OK] ... checkout matches: all 6 checksums`. That is\n  expected: the content check reads your local checkout, the tag check queries the real\n  remote. It clears the moment you push.\n- It chains into `sync-checksums.sh`, so run it **after** step 3's checksums are committed,\n  or the dist manifest gets stale hashes.\n\n---\n\n## 10c. Windows + self-hosted-runner traps (read before debugging a red Windows job)\n\nThese all cost a full CI cycle. The unifying fact:\n\n> The self-hosted runner service executes as `NT AUTHORITY\\NETWORK SERVICE`, which has its\n> own PATH **and** its own NTFS permissions. Anything you verify over SSH on that box tells\n> you nothing about what the job can do. `native_win_arm64_qhexrt` already carries a long\n> comment about this — read it before inventing a fix, and keep it in sync with neurun's\n> equivalent lane, which runs on the *same machine*.\n\n**Environment**\n\n- **`bash: command not found`** — Git for Windows puts `Git\\cmd` (git.exe) on PATH but not\n  `Git\\bin` (bash.exe). `actions/checkout` still works because it calls git.exe directly,\n  which makes this look unrelated to runner setup.\n- **`pwsh` does not exist on that box** — Windows PowerShell 5.1 only. Use\n  `shell: powershell`.\n- **`gh` is not installed on that box, and must not be assumed anywhere.** A QHexRT\n  download died on `gh: command not found`. The fix is deliberately *not* to install it:\n  every fetch of a private release asset now goes through\n  `fetch_release_asset()` in `scripts/build/_release_asset.sh`, which uses `curl` + the\n  REST API. `curl` ships with Windows, macOS and every Linux image we use, so this is one\n  code path everywhere instead of a per-runner provisioning step. If you add a new\n  consumer of a private asset, source that helper — do not reach for `gh`.\n  Note the helper matches the asset name **exactly**: a prefix match picks the `.sha256`\n  sidecar (its name starts with the tarball's), and you get a 65-byte \"archive\" that fails\n  much later with a confusing error.\n- **Tool availability is the single most common failure class on this runner** — `bash`,\n  `python3`, `ninja`, `gh`, GNU `tar` and NDK `.exe` suffixes have each broken a lane\n  once. When a self-hosted job fails, check \"does this tool exist as spelled\" before\n  reading any logic.\n- **`ln -s` under Git-Bash COPIES the directory** unless `MSYS=winsymlinks:nativestrict`.\n  It does not fail, so the result looks like real content and whatever validates a\n  \"must be a symlink\" invariant rejects a payload that was actually fine. Never use shell\n  `ln -s` on this box — go through `scripts/build/_selection.py`.\n- **Windows selects with a JUNCTION, not a symlink, and that is deliberate.** The runner\n  service account cannot create symlinks (below), but a junction is the same kind of\n  reparse point to a directory and needs no privilege. Two traps when reading that code:\n  `Path.is_symlink()` returns **False** for a junction (`stat.S_ISLNK` is true only for\n  `IO_REPARSE_TAG_SYMLINK`) — use `_selection.is_selection()`; and a junction stores an\n  **absolute** t",".claude/skills/engine-plugin-authoring/SKILL.md":"---\nname: engine-plugin-authoring\ndescription: Step-by-step recipe for adding a new engine plugin under runanywhere-sdks/engines/, or adding a modality (e.g. TTS) to an existing engine (llamacpp, sherpa, onnx, cloud, mlx, neurt, qhexrt). Use when the task is \"add engine X\", \"wire up backend Y\", or \"make engine Z also serve <modality>\".\n---\n\n# Engine plugin authoring (`engines/`)\n\nTwo related runbooks: adding a brand-new engine plugin, and adding a modality to one\nthat already exists. Read `engines/AGENTS.md` first for the concepts these steps\nassume (op-table vtable, manifest, the valid-engine checklist, the 4-file skeleton,\nthe engine↔runtime rule). This skill is the \"how\", that file is the \"what/why\".\n\n**Invariant that applies to both procedures:** never invent a new modality just to\nship an engine — fill an existing `rac_engine_vtable_t` slot. Adding a brand-new\n*primitive* (a new `RAC_PRIMITIVE_*` + vtable slot) is a commons ABI change, covered\nby `core/AGENTS.md` → \"Adding a new capability interface\", not an engine change.\n\n---\n\n## Procedure A — add a new engine\n\nMirror **sherpa** (multi-modality, standalone `.so`) or **cloud** (single modality,\nno runtime) as your template; both are the cleanest references for the 4-file\nskeleton described in `engines/AGENTS.md`.\n\n1. **Create `engines/<name>/`** with the 4-file skeleton: `rac_plugin_entry_<name>.cpp`,\n   `rac_backend_<name>_register.cpp` (skip it if there's no extra bring-up, like\n   `neurt`), `rac_static_register_<name>.cpp`, plus your impl files.\n2. **Write the manifest** in `rac_plugin_entry_<name>.cpp`:\n   - `name` = snake_case identity (the library/framework you wrap, or your own\n     codebase unit — NEVER a modality name; see the naming rule in\n     `engines/AGENTS.md`).\n   - `primitives[]` = what you serve; fill the matching vtable slots; leave every\n     other slot explicit NULL (the all-NULL tripwire depends on this).\n   - `runtimes[]` — only devices your compute actually depends on (NULL for\n     cloud/HTTP engines) — see THE RULE in `engines/AGENTS.md`.\n   - `formats[]` = the `RAC_MODEL_FORMAT_ID_*` values you accept (NULL if there's\n     no local model file).\n   - `priority`, `availability` (PUBLIC/PRIVATE), `package_owner`/`package_name`.\n3. **Implement the op-table(s)** honoring the MODEL LIFECYCLE:\n   `create → initialize → use → cleanup → destroy` (see `engines/AGENTS.md` for the\n   exact signatures). Use `RAC_DEFINE_CREATE_ADAPTER(primitive, name)`\n   (`core/include/rac/plugin/rac_plugin_entry.h`) when `create` is a plain forward\n   onto `rac_<primitive>_<name>_create` — sherpa uses it for STT/TTS/VAD. Engines\n   with richer create flows (llamacpp, onnx, neurt, qhexrt) hand-write it.\n4. **`capability_check`** when the engine is platform- or binary-gated: use the\n   shared 3-way helper `rac_engine_unavailable_capability(platform_supported,\n   backend_present)` from `engines/common/rac_engine_unavailable.h`. When the\n   backend can be entirely absent at build time (a private prebuilt archive, a\n   sibling private repo), emit the stub with `RAC_ENGINE_UNAVAILABLE_PLUGIN(name,\n   display, cap_fn)` from the same header — see qhexrt and neurt for the two-mode\n   (ROUTABLE vs STUB) pattern this produces.\n5. **Registration carriers:** `RAC_PLUGIN_ENTRY_DEF(<name>)` returning the vtable; a\n   `rac_backend_<name>_register()`/`_unregister()` pair if you have extra bring-up\n   (called directly by SDK bridges on dynamic-link hosts); a\n   `rac_static_register_<name>.cpp` shim using `RAC_STATIC_REGISTER_BACKEND(<name>)`\n   (routes through the register fn) or `RAC_STATIC_PLUGIN_REGISTER(<name>)` (calls\n   the entry directly) for iOS/WASM static hosts.\n6. **CMake:** call `rac_add_engine_plugin(<name> SOURCES … LINK_LIBRARIES …\n   AVAILABILITY … PACKAGE_OWNER … PACKAGE_NAME …)` (`cmake/plugins.cmake`) from\n   `engines/<name>/CMakeLists.txt`, fronted by your own\n   `option(RAC_BACKEND_<NAME> \"…\" <default>)` + `if(NOT RAC_BACKEND_<NAME>)\n   return() endif()` self-gate (each engine owns its own option — `engines/CMakeLists.txt`\n   just unconditionally `add_subdirectory()`s and lets the child decide). Add\n   `add_subdirectory(<name>)` there. Declare `primitives`/`runtimes`/`formats` ONLY\n   in the C manifest, never in CMake, so the build graph can't drift from what the\n   router routes.\n7. **Android:** if the SDK loads the engine via `System.loadLibrary` +\n   `nativeRegister`, add a JNI bridge with `RAC_DEFINE_ENGINE_JNI_BRIDGE(...)`\n   (standalone per-engine `.so` — onnx, llamacpp) or\n   `RAC_DEFINE_ENGINE_JNI_BRIDGE_NO_ONLOAD(...)` (folded into a host lib that\n   already owns `JNI_OnLoad`, e.g. cloud → `librunanywhere_jni.so`) from\n   `engines/common/rac_engine_jni_bridge.h`. The JVM class-path token in the macro\n   call must match the Kotlin `*Bridge` class byte-for-byte.\n8. **iOS/WASM static hosts:** make sure the host force-loads the plugin —\n   `rac_force_load(<app> PLUGINS <name>)` in the host's CMakeLists — so the\n   static-init Registrar TU survives linker stripping.\n9. **If this engine ships an Electron/npm carrier package**, register it in\n   `scripts/validation/gates/check_plugin_natives.py`'s `REQUIRED_OPS` dict (the\n   ops-table symbol name(s) your entry TU references only on the routable\n   branch) — `package-sdk.sh` calls this automatically, so an unregistered\n   engine's stub/shell builds ship undetected. **Check which FILE actually\n   compiles `rac_plugin_entry_<name>.cpp`** before assuming the carrier\n   (`runanywhere_<name>`) is what to gate: sherpa/onnx/llamacpp compile it into\n   the carrier itself (an undefined symbol reference there is the evidence),\n   but neurt's `rac_add_engine_plugin()` call puts it on the ENGINE target\n   (`rac_backend_neurt`) instead — the carrier there is empty boilerplate with\n   zero symbols. Add the engine id to `EVIDENCE_IN_BACKEND_FILE` if it follows\n   neurt's shape; discovered by actually building neurt for real and running\n   `nm -a` on both files, not by reading the CMakeLists and assuming.\n\n## Procedure B — add a modality to an existing engine\n\nMulti-modality is the whole point of an identity-named engine. `cloud` and `neurt`\nare explicitly built for this and carry the recipe inline as a comment next to\ntheir vtable.\n\n1. Implement the new modality's op-table (e.g. a cloud TTS `g_cloud_tts_ops`,\n   typically backed by a per-modality provider adapter under `providers/`).\n2. **Fill the corresponding slot** in the engine's `rac_engine_vtable_t`\n   (`tts_ops` / `llm_ops` / `embedding_ops` / …) — every other slot must stay\n   explicit NULL.\n3. **Add the primitive** to the manifest's `primitives[]`.\n4. Update `formats[]`/`runtimes[]` only if the new modality actually needs them\n   (per THE RULE — don't declare a runtime your new op-table doesn't depend on).\n\nNo new plugin, no rename, no ABI bump — the engine already owns one\n`rac_engine_vtable_t` with all ten primitive slots; you're just filling another one.\nGrep the target engine's `rac_plugin_entry_<name>.cpp` for the phrase \"To add a\"\n— cloud and neurt both leave a comment naming exactly which slots are still free\nand what to add to `primitives[]`.\n",".agents/skills/sdk-publish/SKILL.md":"---\nname: sdk-publish\ndescription: Manually publish an already-tagged, already-released (non-draft GitHub Release) RunAnywhere SDK version to every package registry (npm, Maven Central, pub.dev, Swift SPM dist repo) plus macOS rcli notarization, with heavy verification that nothing fake/stubbed/broken ships.\n---\n\n# SDK Publish\n\nRepo: `~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks` (verify with `pwd`/`git remote get-url origin` — do not assume this path is still correct, checkouts move). Version source of truth: `core/VERSION`. Throughout, `$VERSION` means the bare semver being published (e.g. `0.20.24`), never prefixed with `v` except in git tags / GitHub Release names.\n\n## THIS PROCESS IS INTENTIONALLY MANUAL\n\n**`release.yml` never publishes anywhere public.** It only builds artifacts, packages them, and creates a draft/published **GitHub Release**. It does not call `npm publish`, does not upload to Maven Central, does not call `flutter pub publish`, and does not push the Swift dist repo. Verify this is still true before trusting it — greps below should all return **zero** matches (grep the whole file, not a snippet):\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks\ngrep -inE \"npm publish|publishReleasePublication|publishAllPublicationsToMavenCentral|flutter pub publish\" .github/workflows/release.yml\n```\n\nThis is a **deliberate safety gate**, not a gap. Every step below is a human (or an agent acting on a human's explicit go-ahead) pushing a button that CI intentionally never pushes. Do not propose wiring any of this into CI as a \"fix\" — that would remove the gate.\n\n## Precondition check (always run first)\n\nRefuse to proceed if the release is a draft or the asset set looks incomplete.\n\n```bash\ngh release view v$VERSION --json isDraft,tagName --jq '{isDraft, tagName}'\n# isDraft MUST be false.\n\ngh release view v$VERSION --json assets --jq '.assets[].name' | sort\n```\n\nCross-reference the listing against the `sdk-release` skill's step 8 asset-count expectations (`~/.claude/skills/sdk-release/SKILL.md`). As a second, independent check, also diff against the **previous** tagged release's asset list — same count, same name patterns, only the version token differs:\n\n```bash\ngh release view v<PREVIOUS_VERSION> --json assets --jq '.assets[].name' | sed \"s/<PREVIOUS_VERSION>/$VERSION/\" | sort >/tmp/expected.txt\ngh release view v$VERSION --json assets --jq '.assets[].name' | sort >/tmp/actual.txt\ndiff /tmp/expected.txt /tmp/actual.txt\n```\n\nAs of the last verified run (**v0.20.25, 65 assets** — up from 55 at v0.20.24 because the 5 public Electron tarballs and their sidecars joined the release; see §6), the full set is: `MANIFEST.txt`; per-platform `RACommons-{android-arm64-v8a,android-armeabi-v7a,android-x86_64,ios,linux-x86_64,web,windows-x64}-v$VERSION.{zip,tar.gz}` + `.sha256`; iOS backend zips `RABackend{LLAMACPP,MLX,NeuRT,ONNX,Sherpa}-ios-v$VERSION.zip` + `.sha256`; `RunAnywhereMLX{Metal,Resources,Runtime}-ios-v$VERSION.zip` + `.sha256`; `rcli-{linux-x86_64,macos-arm64,windows-x86_64}-v$VERSION.tar.gz`/`.zip` + `.sha256`; the 8 npm tarballs `runanywhere-{core,llamacpp,mlx,onnx,proto-ts,web,web-llamacpp,web-onnx}-$VERSION.tgz` + `.sha256`; the **5 Electron npm tarballs** `runanywhere-electron{,-llamacpp,-onnx,-sherpa,-neurt}-$VERSION.tgz` + `.sha256` (new as of v0.20.25); and `runanywhere-kotlin-maven-v$VERSION.zip` + `.sha256`. There is **no** Flutter tarball in the release (Flutter packages are staged from the native archives above, not from a dedicated release asset) and **no** `.dmg` asset in the baseline set (the macOS rcli tarball is what ships; see the notarization section for when a `.dmg` does get produced). If a `qhexrt`-named asset appears anywhere in this list, stop — that would mean `release.yml`'s own manifest assertion should have hard-failed and something is very wrong; do not publish anything from that release.\n\n## 0. MANDATORY GATE — verify the ENTIRE release before pushing anything to any registry\n\n**Run this once, in full, before the first registry push. Do not publish a single package until it passes.** Per-package spot-checks are not a substitute: they are how a bad artifact reaches a registry you cannot un-publish from. Proven end to end on v0.20.25 (2026-08-22), then independently audited and corrected: **34 payloads, 6619 files across 3 nesting levels, 282 binaries, 0 placeholder hits, 38/38 commons binaries carrying the real staging host**. The audit found 4 methodology gaps in the first pass (shallow nesting, missing `MTLB` magic, `.lib` excluded from the positive check, and a set-intersection that could pass vacuously) — every one is called out inline below, because each produced a *confident but under-covered* result.\n\n```bash\nVERSION=0.20.25                      # no leading v\nREPO=RunanywhereAI/runanywhere-sdks\nW=/tmp/relverify-$VERSION\nrm -rf \"$W\" && mkdir -p \"$W/assets\" \"$W/qhexrt\" \"$W/x\" \"$W/nested\"\n\n# (1) EVERY release asset — no -p filter, get the whole set.\ngh release download \"v$VERSION\" --repo \"$REPO\" -D \"$W/assets\"\n\n# (2) The PRIVATE QHexRT artifact is NOT a release asset — pull it from the run.\n#     Find the run that produced the release, then:\ngh run download <release-run-id> --repo \"$REPO\" --name sdk-electron-qhexrt-private --dir \"$W/qhexrt\"\n```\n\n### (3) Checksums — `cd` into the directory first\n\nSidecars contain a **bare basename**, so `shasum -c` fails with a misleading \"FAILED open or read\" if run from anywhere else. This will make every artifact look broken when nothing is wrong:\n\n```bash\ncd \"$W/assets\"\nok=0; bad=0\nfor s in *.sha256; do\n  if shasum -a 256 -c \"$s\" >/dev/null 2>&1; then ok=$((ok+1)); else echo \"MISMATCH: $s\"; bad=$((bad+1)); fi\ndone\necho \"verified=$ok mismatched=$bad\"      # bad MUST be 0\n(cd \"$W/qhexrt\" && shasum -a 256 -c ./*.sha256)\n```\n\n### (4) Extract everything — INCLUDING nested archives\n\n**The gotcha that matters:** `runanywhere-kotlin-maven-v*.zip` contains `.aar` files, and the `.aar`s are what hold the Android `.so` binaries. A one-level extraction reports \"0 binaries\" for the Maven bundle and every subsequent scan passes **vacuously**.\n\n**Recurse until the tree is dry — TWO levels are required here, not one.** Each `.aar` also contains a `classes.jar`, holding ~2027 `.class` files that are doubly unreachable: inside an unopened jar *and* excluded by the `.class` suffix rule most magic-byte scanners use. Level 2 adds **2033 files** (0 placeholder hits on v0.20.25); level 3 is empty. Loop until a pass finds no new archives rather than hardcoding a depth.\n\n```bash\ncd \"$W/assets\"\nfor f in *; do\n  case \"$f\" in *.sha256|MANIFEST.txt) continue;; esac\n  d=\"$W/x/$f\"; mkdir -p \"$d\"\n  case \"$f\" in\n    *.zip)              unzip -qq -o \"$f\" -d \"$d\" || echo \"EXTRACT FAILED: $f\" ;;\n    *.tar.gz|*.tgz)     tar -xzf \"$f\" -C \"$d\"     || echo \"EXTRACT FAILED: $f\" ;;\n    *) echo \"UNHANDLED TYPE: $f\" ;;\n  esac\ndone\nd=\"$W/x/$(basename \"$W\"/qhexrt/*.tgz)\"; mkdir -p \"$d\"; tar -xzf \"$W\"/qhexrt/*.tgz -C \"$d\"\n\n# nested pass\ncd \"$W/x\"\nfind . -type f \\( -name '*.aar' -o -name '*.jar' -o -name '*.zip' -o -name '*.tgz' -o -name '*.tar.gz' \\) \\\n  | sed 's|^\\./||' | while IFS= read -r a; do\n    dd=\"$W/nested/$(printf '%s' \"$a\" | tr '/' '_')\"; mkdir -p \"$dd\"\n    case \"$a\" in *.aar|*.jar|*.zip) unzip -qq -o \"$a\" -d \"$dd\";; *) tar -xzf \"$a\" -C \"$dd\";; esac\n  done\n```\n\n**Prove extraction actually happened** — an empty dir makes \"0 hits\" meaningless:\n\n```bash\nfor d in \"$W\"/x/*/; do\n  n=$(find \"$d\" -type f | wc -l)\n  [ \"$n\" -lt 2 ] && echo \"SUSPICIOUSLY EMPTY: $d ($n files)\"\ndone\n```\n\n### (5) Placeholder scan — EVERY file, not just binaries\n\nThe staging URL can also live in JS/JSON/TS config, so scan by content across all file types, and additionally scan the raw compressed streams in case a member never got extracted:\n\n```bash\nfind \"$W/x\" \"$W/nested\" -type f | wc -l                                     # total files covered\ngrep -rla \"YOUR_STAGING_BASE_URL\" \"$W/x\" \"$W/nested\" | tee /tmp/ph.txt | wc -l   # MUST be 0\nfor f in \"$W\"/assets/*.tgz \"$W\"/assets/*.tar.gz \"$W\"/qhexrt/*.tgz; do\n  gzip -dc \"$f\" 2>/dev/null | grep -qa \"YOUR_STAGING_BASE_URL\" && echo \"RAW HIT: $f\"\ndone\n```\n\nFind binaries by **magic bytes, not extension** (an unsuffixed framework binary like `RACommons.framework/RACommons` has no extension and a name-based `find` misses it). Signatures to cover:\n\n| Magic | Kind | v0.20.25 count |\n|---|---|---|\n| `7f454c46` | ELF | 177 |\n| `!<arch>` (`213c617263683e0a`) | static `.a` **and `.lib`** | 41 |\n| `MZ` | PE | 29 |\n| `cffaedfe` / `cefaedfe` | Mach-O | 19 |\n| `00 61 73 6d` | wasm | 10 |\n| **`4d544c42` (`MTLB`)** | **Metal library** — easy to miss | 5 |\n| `cafebabe` / `bebafeca` | fat Mach-O (exclude `.jar`/`.class`) | 1 |\n\n**Total: 282 binaries.** An earlier pass reported 277 because `MTLB` was absent from the signature list — `.metallib` files were invisible to the binary scan (they were still covered by the all-file `grep` in step 5, so the conclusion held, but the count was wrong). If your total comes in materially under ~282 for a comparable release, your detection is leaking.\n\n### (6) POSITIVE staging-URL check — absence of the placeholder is NOT sufficient\n\nA build with an **empty** URL baked in passes a placeholder-only check. `check_plugin_natives.py` only tests absence, so add the positive assertion yourself.\n\nTwo traps that produced false confidence on the v0.20.25 pass, both worth avoiding:\n\n- **Include `.lib`.** A `MUST_HAVE` regex of `rac_commons\\.(dylib|so|dll|a)$` silently skips\n  `RACommons-windows-x64/x64/rac_commons.lib` — an **89 MB** binary that then gets no positive\n  check at all. Match `(dylib|so|dll|a|lib)` plus `\\.wasm$`. Correct coverage is **38** binaries,\n  not 37.\n- **Assert per binary, not by set-intersection.** Intersecting \"non-public origins across all\n  files\" yields more than one origin here (`hf.co` *and* the staging host), so a binary carrying\n  only `hf.co` would pass **vacuously**. Check each binary individually for the staging host.\n\n```bash\n# v0.20.25 baseline: 38 commons/wasm/lib binaries; 0 missing the staging host.\n```\n\nNever print the URL — it is a repo secret. Report a truncated SHA-256 fingerprint, or just assert presence/absence counts.\n\n### (7) Routability gate over the whole tree\n\n```bash\npython3 scripts/validation/gates/check_plugin_natives.py \"$W/x\" \"$W/nested\" > /tmp/gate.txt 2>&1\ngrep -cE '^  ok ' /tmp/gate.txt; grep -E '^  FAIL' /tmp/gate.txt\n```\n\nv0.20.25 baseline: **77 ok, 1 FAIL**. The single expected FAIL is\n`RACommons-linux-x86_64/lib/librunanywhere_sherpa.so` — see the known-exception note below. **Any other FAIL blocks the publish.**\n\n### (8) Known exception — Linux sherpa is a non-routable shell (PRE-EXISTING, not a regression)\n\n`RACommons-linux-x86_64-v*.tar.gz` ships **no** `libsherpa-onnx-c-api.so` at all and its `librac_backend_sherpa.so` is a ~156 KB shell (~3 sherpa strings, vs ~31 MB / ~192 for a healthy one on Electron macOS) — the engine is not linked in, so the carrier cannot reference `g_sherpa_{stt,tts,vad}_ops` and **STT/TTS/VAD cannot work from that bundle**. Android's carriers pass on all 3 ABIs, so this is Linux-only.\n\n**Compare the CARRIER, not the backend.** The gate FAILs on `librunanywhere_sherpa.so` (the carrier); citing `librac_backend_sherpa.so` as your evidence is comparing a *different file* and is not proof. The carrier is byte-identical across releases, which is the real proof:\n\n```bash\ngh release download \"v<PREV>\" --repo \"$REPO\" -p 'RACommons-linux-x86_64-v<PREV>.tar.gz' -D /tmp/prev\ntar -xzf /tmp/prev/*.tar.gz -C /tmp/prev\nshasum -a 256 /tmp/prev/lib/librunanywhere_sherpa.so \"$W\"/x/RACommons-linux-*/lib/librunanywhere_sherpa.so\n# v0.20.24 and v0.20.25 both: size=15712  sha256=28ef16015b61da86...\n\n# strongest check — run the gate on the PREVIOUS bundle and confirm the identical FAIL:\npython3 scripts/validation/gates/check_plugin_natives.py /tmp/prev\n```\n\nIf it ever regresses *further* (e.g. Android or macOS sherpa also goes hollow), stop and treat it as a blocking defect.\n\n### (9) Account for every payload explicitly\n\nPrint a per-artifact table (checksum verdict, binary count, placeholder hits) and confirm the row count equals the payload count. Artifacts that legitimately contain **no** native binaries, and why — do not let these pass silently unexamined:\n\n| Artifact | Why it has no binaries |\n|---|---|\n| `RunAnywhereMLXResources-ios-*.zip` | 25 third-party license/notice text files |\n| `runanywhere-kotlin-maven-v*.zip` | binaries live in the **nested** `.aar`s (step 4) |\n| `runanywhere-proto-ts-*.tgz` | pure TS/JS (69 `.ts` + 69 `.js`) |\n\nEverything else must have a non-zero, plausible binary count. A platform suddenly shipping fewer binaries than the previous release is a regression signal worth chasing before publishing.\n\n### (10) Blind spot to be aware of — Windows x64 has NO routability coverage\n\n`RACommons-windows-x64-v*.zip` ships **only** a static `rac_commons.lib` (~89 MB) plus 152 `.h` / 11 `.hpp` headers — **zero backend DLLs**, where Linux/Android/Web each ship 4–5 backend carriers. That is consistent with a static-embedding bundle (consumers link the engines in themselves), but the consequence matters: **no gate can detect a missing or hollow Windows backend**, because there is no carrier to inspect. `check_plugin_natives.py` therefore reports nothing at all for this artifact rather than reporting health.\n\nWindows x64 is also *advisory* in `release.yml` (its absence does not fail the release). So a Windows regression can pass every automated check. If Windows correctness matters for a given release, verify it by actually running something on the real Windows box (see §Electron for how to reach it) — do not infer it from a green pipeline. Worth a deliberate confirmation with the repo owner that the DLL-free shape is intentional.\n\n---\n\n## General verification discipline (every artifact, every registry)\n\nApply all three checks below to **every** file you're about to publish, before AND after the publish call. Never trust a copy cached locally earlier in the session — re-download.\n\n**(a) Download + verify the sha256 sidecar yourself:**\n\n```bash\ngh release download v$VERSION -p '<file>' -p '<file>.sha256' -D /tmp/sdk-publish-$VERSION\ncd /tmp/sdk-publish-$VERSION && shasum -a 256 -c '<file>.sha256'\n```\n\n**(b) Inspect the actual file listing inside every tarball/AAR/zip** — never assume from the filename alone:\n\n```bash\nnpm pack <tgz-or-package-dir> --dry-run     # npm packages: prints the exact file list it would publish\nunzip -l  <file.zip>                        # AARs, Maven repo zip, native archives\ntar -tzf  <file.tar.gz>                     # rcli, RACommons tarballs\n```\n\nSanity-check what comes back:\n- No suspiciously-small or empty binaries where a real native library (`.so`, `.dylib`, `.a`, `.dll`) should be — a stub or a zero-byte placeholder means the build silently failed upstream.\n- No leftover `workspace:*` or `file:` dependency specs in any `package.json` inside the tarball — those only resolve inside this monorepo and break for a real external installer.\n- Version strings inside every manifest (`package.json` `version`, AAR `pom.xml` `<version>`, `pubspec.yaml` `version:`) **exactly** match `$VERSION`.\n- **Hard rule, no exceptions:** grep the full file listing for `qhexrt` (any case) and for QNN binary names (`libQnnHtp`, `QnnHtp*.dll`, `rac_commons.dll` bundled with QHexRT, `runanywhere_qhexrt`). If any hit appears in a package meant to be public, stop and do not publish that artifact.\n\n**(c) Confirm baked-in runtime config** (e.g. a staging-vs-prod backend base URL compiled into `rac_commons` at build time) points at the real intended endpoint. This is designed to fail closed if misconfigured — a build that completed and produces working requests against prod is itself decent evidence — but if you have any doubt, `strings <binary> | grep -i \"api\\.\\|staging\\.\"` and eyeball it.\n\n## 1. npm — 8 packages, order matters\n\nPackages: 4 Web (`@runanywhere/proto-ts`, `@runanywhere/web`, `@runanywhere/web-llamacpp`, `@runanywhere/web-onnx`) + 4 React Native (`@runanywhere/core`, `@runanywhere/llamacpp`, `@runanywhere/mlx`, `@runanywhere/onnx`). Never `@runanywhere/qhexrt` (RN) — it exists in `bindings/react-native/packages/qhexrt/` but is excluded from the public npm train by the same policy as Maven/pub.dev QHexRT.\n\n**Publish `@runanywhere/proto-ts` FIRST.** `@runanywhere/web-onnx` deliberately does not bundle its own `proto-ts` copy and needs it resolvable from the live registry for real consumers; publish `proto-ts` before it or a fresh `npm install @runanywhere/web-onnx` fails to resolve.\n\n```bash\ngh release download v$VERSION -p 'runanywhere-proto-ts-*.tgz*' -p 'runanywhere-web-*.tgz*' \\\n  -p 'runanywhere-core-*.tgz*' -p 'runanywhere-llamacpp-*.tgz*' -p 'runanywhere-mlx-*.tgz*' -p 'runanywhere-onnx-*.tgz*' \\\n  -D /tmp/sdk-publish-$VERSION\ncd /tmp/sdk-publish-$VERSION\nfor f in *.sha256; do shasum -a 256 -c \"$f\"; done\n\nnpm publish runanywhere-proto-ts-$VERSION.tgz --access public   # FIRST, always\nnpm publish runanywhere-web-$VERSION.tgz --access public\nnpm publish runanywhere-web-llamacpp-$VERSION.tgz --access public\nnpm publish runanywhere-web-onnx-$VERSION.tgz --access public\nnpm publish runanywhere-core-$VERSION.tgz --access public\nnpm publish runanywhere-llamacpp-$VERSION.tgz --access public\nnpm publish runanywhere-mlx-$VERSION.tgz --access public\nnpm publish runanywhere-onnx-$VERSION.tgz --access public\n```\n\n`npm whoami` should already show a logged-in account; if it errors, stop and tell the human rather than trying to log in non-interactively.\n\n**GOTCHA — large tarballs can silently die mid-upload.** `@runanywhere/mlx` (~50MB) and `@runanywhere/web-onnx` (unpacks to 300+MB) can exceed a short default shell/tool timeout mid-upload and get killed with no clean error. Use an explicit long timeout (5 minutes / 300000ms) on each publish call.\n\n**GOTCHA — verify via the registry API, not the npm CLI.** `npm view` can lag the CLI's own cache by several seconds right after a fresh publish and falsely report \"not found\". After **every** publish:\n\n```bash\ncurl -s \"https://registry.npmjs.org/@runanywhere/<pkg>\" | python3 -c \"import json,sys;print(json.load(sys.stdin)['dist-tags'])\"\n```\nConfirm `latest` equals `$VERSION`.\n\n## 2. Maven Central — 3 AARs only\n\n`io.github.sanchitmonga22:{runanywhere-sdk,runanywhere-llamacpp,runanywhere-onnx}`. **Never `runanywhere-qhexrt-android`** — it was published through 0.20.19 under an earlier, since-reversed policy and is now permanently excluded; `release.yml`'s manifest assertion hard-fails if it reappears in the public asset set.\n\nFull step-by-step lives in `bindings/kotlin/docs/KOTLIN_MAVEN_CENTRAL_PUBLISHING.md`, section **\"Option A: Publish with released native archives\"** (line ~108 as of this writing — the doc has been corrected mid-release before, so re-read it fresh rather than trusting memory of its contents). Key points, condensed:\n\n1. Download + verify the 3 `RACommons-android-{arm64-v8a,armeabi-v7a,x86_64}-v$VERSION.zip` from the release (sha256 sidecars, per the general discipline above).\n2. Stage: `bash bindings/kotlin/scripts/package-sdk.sh --mode local --natives-from <dir-with-the-3-zips>`. This also produces a local Maven-repo zip — diff its size against `runanywhere-kotlin-maven-v$VERSION.zip` already attached to the GitHub Release as a cheap reproducibility check.\n3. Before uploading, confirm the OSSRH compatibility service has no dangling open repository for this namespace:\n   ```bash\n   CENTRAL_BEARER=\"$(printf '%s:%s' \"$MAVEN_CENTRAL_USERNAME\" \"$MAVEN_CENTRAL_PASSWORD\" | base64 | tr -d '\\r\\n')\"\n   curl -s -H \"Authorization: Bearer $CENTRAL_BEARER\" \\\n     'https://ossrh-staging-api.central.sonatype.com/manual/search/repositories?ip=client&profile_id=io.github.sanchitmonga22'\n   ```\n   Resolve any stale repository per the doc's troubleshooting table before continuing.\n4. Source `MAVEN_CENTRAL_USERNAME`/`MAVEN_CENTRAL_PASSWORD` from `~/.gradle/gradle.properties` (`mavenCentral.username`/`mavenCentral.password` — confirmed present on this machine; never print the values) and run, from `bindings/kotlin/`:\n   ```bash\n   ./gradlew clean \\\n     :publishReleasePublicationToMavenCentralRepository \\\n     :modules:runanywhere-core-llamacpp:publishReleasePublicationToMavenCentralRepository \\\n     :modules:runanywhere-core-onnx:publishReleasePublicationToMavenCentralRepository \\\n     -Prunanywhere.useLocalNatives=true -x buildLocalJniLibs --no-daemon\n   ```\n   GPG signing (`signing.gnupg.keyName`/`signing.gnupg.passphrase` in the same properties file, key id `CC377A9928C7BB18` — confirmed importable via `gpg --list-secret-keys --keyid-format LONG` on this machine) is read from the same properties file automatically; you do not pass it separately.\n5. Transfer to Central Portal:\n   ```bash\n   curl --fail --request POST -H \"Authorization: Bearer $CENTRAL_BEARER\" \\\n     'https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/io.github.sanchitmonga22?publishing_type=automatic'\n   ```\n   Then find the resulting `portal_deployment_id` — expect **exactly one** fresh repository; if the count isn't 1, stop and investigate rather than guessing which one is yours (see the doc for the exact `jq` filter).\n6. Poll `https://central.sonatype.com/api/v1/publisher/status?id=<id>` (POST, same bearer) until `deploymentState` is `PUBLISHED`. Automatic deployments normally go `PENDING -> VALIDATING -> PUBLISHING -> PUBLISHED` within a few minutes. **If it lands on `VALIDATED` instead of proceeding, that means Sonatype is waiting for a manual click in the Central Portal UI** — stop and tell the human; do not loop forever assuming it resolves itself. On `FAILED`, print `.errors` and stop.\n7. Verify propagation (allow 10-30 minutes for CDN propagation even after `PUBLISHED`):\n   ```bash\n   for a in runanywhere-sdk runanywhere-llamacpp runanywhere-onnx; do\n     echo \"$a: $(curl -s -o /dev/null -w '%{http_code}' \"https://repo1.maven.org/maven2/io/github/sanchitmonga22/$a/$VERSION/$a-$VERSION.pom\")\"\n   done\n   ```\n   Expect `200` for all three.\n\n## 3. pub.dev (Flutter)\n\n**Verify the current package set fresh — do not trust a hardcoded count.** As of this writing the real, non-QHexRT Flutter package set is **4**, not 3: `runanywhere`, `runanywhere_llamacpp`, `runanywhere_onnx`, and `runanywhere_mlx` (mlx was added after some earlier documentation/memory of \"3 packages\" was written — that assumption is stale; `runanywhere_mlx` is a real, already-published-on-pub.dev package, not new/WIP). Enumerate it yourself instead of trusting this list verbatim next time:\n```bash\nfind bindings/flutter/packages -maxdepth 1 -type d -not -iname '*qhexrt*' -not -path '*/packages'\n```\n`runanywhere_qhexrt` exists in that directory too — never publish it (same exclusion policy as Maven).\n\nStage the released iOS xcframeworks + Android `.so` into each package with the Flutter equivalent of `package-sdk.sh` (melos-based monorepo, confirmed at `bindings/flutter/scripts/package-sdk.sh`):\n\n```bash\nbash bindings/flutter/scripts/package-sdk.sh --mode local --natives-from <dir-with-xcframeworks-and-android-.so>\n```\n\nThis script already runs `flutter pub publish --dry-run` internally per package as part of its own validation — but run it again yourself, per package, from each package directory, and **read the output for real, not just the exit code**:\n\n```bash\ncd bindings/flutter/packages/runanywhere && flutter pub publish --dry-run\ncd ../runanywhere_llamacpp && flutter pub publish --dry-run\ncd ../runanywhere_onnx && flutter pub publish --dry-run\ncd ../runanywhere_mlx && flutter pub publish --dry-run\n```\n\nExpect zero warnings except a benign version-gap hint. Only then run the real publish, same order (core first, since the others depend on it):\n\n```bash\ncd bindings/flutter/packages/runanywhere && flutter pub publish\ncd ../runanywhere_llamacpp && flutter pub publish\ncd ../runanywhere_onnx && flutter pub publish\ncd ../runanywhere_mlx && flutter pub publish\n```\n\n**Credential note:** pub.dev OAuth is normally cached at `~/Library/Application Support/dart/pub-credentials.json` on macOS (confirmed present on this machine). If it's missing, expired, or `flutter pub publish` prompts for an interactive browser login, **STOP and tell the human** — you cannot complete a fresh Google OAuth flow non-interactively, and forcing/guessing here is not appropriate.\n\nVerify afterward against the live API (not a cached page):\n```bash\nfor p in runanywhere runanywhere_llamacpp runanywhere_onnx runanywhere_mlx; do\n  echo \"$p: $(curl -s \"https://pub.dev/api/packages/$p\" | python3 -c \"import json,sys;print(json.load(sys.stdin)['latest']['version'])\")\"\ndone\n```\n\n## 4. Swift SPM distribution repo (`RunanywhereAI/runanywhere-swift`)\n\nA separate, **generated-only** repo mirroring `bindings/swift` (Package.swift + Sources/ + LICENSE + README) so SwiftPM consumers using `from: \"<version>\"` don't clone the ~340MB monorepo.\n\n**GOTCHA:** this machine may have local directories literally named `runanywhere-swift` that are just checkouts of the **monorepo's** remote, not the distribution repo. Verify before trusting any existing local directory by name:\n```bash\ngit -C <candidate-dir> remote get-url origin\n```\nMust show `RunanywhereAI/runanywhere-swift`, not `runanywhere-sdks`. Always clone fresh instead of guessing:\n\n```bash\ngit clone https://github.com/RunanywhereAI/runanywhere-swift.git /tmp/ra-swift\n\nbindings/swift/scripts/sync-dist-repo.sh \\\n  --zips <exact-verified-Apple-archive-dir-from-the-release> --tag /tmp/ra-swift\n\nRUNANYWHERE_SWIFT_DIST_REPO=/tmp/ra-swift \\\n  bash scripts/validation/gates/check_swift_dist_repo_sync.sh\n\ngit -C /tmp/ra-swift push origin main --follow-tags\n```\n\nThe `--zips` dir must contain the verified (sha256-checked) release archives — `RACommons-ios-v$VERSION.zip`, `RABackend{LLAMACPP,MLX,NeuRT,ONNX,Sherpa}-ios-v$VERSION.zip`, `RunAnywhereMLX{Metal,Resources,Runtime}-ios-v$VERSION.zip` — downloaded and checked exactly per the general verification discipline, not re-used from an earlier, untrusted local copy.\n\n**The tag on `runanywhere-swift` is bare semver, no `v` prefix** (SwiftPM's `from:` needs that) — do not tag it `v$VERSION` by mistake; `sync-dist-repo.sh --tag` handles this correctly, but double check the pushed tag with `git -C /tmp/ra-swift tag --points-at HEAD` before walking away.\n\nThis is enforced, not just documented: once `v$VERSION` is tagged on `runanywhere-sdks`, `check_swift_dist_repo_sync.sh` fails every future PR on the monorepo until `runanywhere-swift` carries the matching bare-semver tag — so don't let this lag behind the other registries.\n\n**GATE GAP — `check_swift_dist_repo_sync.sh` does NOT catch dependency drift, only checksum/version-string drift.** `runanywhere-swift`'s `Package.swift` is manually maintained end to end — `sync-dist-repo.sh` only ever touches `sdkVersion` and the binaryTarget checksums, never the `dependencies:` block (mlx-swift, mlx-swift-lm, mlx-audio-swift, swift-crypto, etc.). If root `runanywhere-sdks/Package.swift` bumps one of those (e.g. a new fork tag), a human has to remember to mirror it into the dist repo by hand, and nothing fails CI if they forget. This exact drift shipped once (2026-08-17): the dist repo pointed `mlx-swift-lm` at unforked `ml-explore/mlx-swift-lm` instead of the `RunanywhereAI` fork, silently breaking every iOS/macOS MLX consumer of that tag until caught by an actual build failure ~40 minutes later. Before trusting a freshly-cut dist-repo tag: diff every `.package(url:` line between root `Package.swift` and the dist repo's `Package.swift` (`grep -n '\\.package(url:' Package.swift` on both sides), and actually build a real product that exercises the dependency you're worried about (e.g. `cd /tmp/ra-swift && swift build --product RunAnywhereMLX`) — don't rely on the sync gate alone. If you catch drift on a tag that's only minutes old with no evidence of external consumption, fixing it in place and force-moving the tag (`git tag -f $VERSION && git push origin $VERSION --force`) is the correct call, not a shortcut — a known-broken immutable tag helps no one.\n\n**If you force-move a tag, SwiftPM/Xcode consumers cache the OLD commit in several places — clearing just `Package.resolved` is not enough.** Verifying your own fix (or having a downstream consumer verify it) can silently keep failing even after the fix is correctly pushed, because: (1) any consumer repo's own tracked `*.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved` pins the exact old resolution and `xcodebuild -resolvePackageDependencies` respects it rather than re-resolving; (2) `~/Library/Caches/org.swift.swiftpm/repositories/<pkg>-<hash>/` is a global shared clone cache keyed by repo, oblivious to a moved tag; (3) `~/Library/org.swift.swiftpm/security/fingerprints/<pkg>-<hash>.json` records the fingerprint SwiftPM saw for that tag and can silently keep using it after a mismatch instead of failing loud. To actually re-resolve after moving a tag: delete the consumer's tracked `Package.resolved`, `rm -rf .build .swiftpm` and any `~/Library/Developer/Xcode/DerivedData/<Scheme>-*`, AND clear the matching entries under both global `org.swift.swiftpm` cache directories for every affected package identity — then resolve fresh.\n\n## 5. macOS rcli notarization\n\nCI (`native_rcli_macos` in `release.yml`) usually already produces a notarized, stapled artifact when Developer ID + notary secrets are configured in repo secrets. **Check the release assets first** before assuming you need to notarize anything yourself locally:\n\n```bash\ngh release view v$VERSION --json assets --jq '.assets[].name' | grep -iE 'rcli-macos|\\.dmg'\n```\nBaseline expectation is `rcli-macos-arm64-v$VERSION.tar.gz` (+ `.sha256`); a `.dmg` only appears when `RCLI_MACOS_FULL_RELEASE=1` was set for that build (see below). If a plain tarball is already present and its contents pass the general verification discipline, there is usually nothing further to do here.\n\n**If you do need to notarize locally**, everything lives in `rcli/scripts/package-rcli.sh` (read its header comment — it documents its own contract in detail):\n\n- One-time setup, check before redoing: `xcrun notarytool history --keychain-profile runanywhere-notary` (confirmed configured on this machine — a fresh submission history returns without error). If missing: `xcrun notarytool store-credentials \"runanywhere-notary\" --apple-id <email> --team-id <team> --password <app-specific-password>`.\n- Codesign identity: `security find-identity -v -p codesigning | grep \"Developer ID Application\"` — confirmed present on this machine as `\"Developer ID Application: RunAnywhere, Inc (<TEAM_ID>)\"`. Grep for it fresh rather than hardcoding the team-id suffix, it can rotate.\n- Invocation contract (env vars, from the script header): `RCLI_CODESIGN_IDENTITY`, `RCLI_MACOS_NOTARIZE=1`, `RCLI_MACOS_FULL_RELEASE=1`, `RCLI_MACOS_SWIFT_BIN_DIR=<output of build-mlx-cli.sh>`. `RCLI_MACOS_FULL_RELEASE=1` is what additionally stages `mlx.metallib` and SwiftPM resource bundles and is what causes a notarized, stapled `.dmg` to be emitted alongside the Homebrew-style tarball. `RCLI_CODESIGN_KEYCHAIN` / `RCLI_NOTARYTOOL_KEYCHAIN` matter only if the identity/profile live in a non-default keychain.\n- Call shape: `rcli/scripts/package-rcli.sh <build-dir> macos-arm64` with the env vars above exported.\n\n## 6. Electron — build AND packaging are now in `release.yml`; publish is still manual\n\n**Reversing the earlier \"deliberately separate\" note**: as of `fix/electron-cicd-staging-url-npu-ane` (2026-08-21), the repo owner asked for Electron to produce ready-to-publish artifacts through the real release train like every other SDK, so `release.yml` now has `native_electron` (invokes `.github/workflows/electron-native-package.yml` as a reusable `workflow_call`) and `sdk_electron` (downloads its `electron-packaged-tarballs` artifact, runs `scripts/release/prepublish_check.py` on every tarball, checksums them, and splits public vs. QHexRT-private). `publish` hard-gates on `sdk_electron` succeeding. `@runanywhere/electron`, `-llamacpp`, `-onnx`, `-sherpa`, `-neurt` land as real, `.sha256`-sidecarred GitHub Release assets, same as the Web/RN npm tarballs. `@runanywhere/electron-qhexrt` still never becomes a public Release asset (policy below) — it's produced and packaged every release, but only reachable as the `sdk-electron-qhexrt-private` workflow artifact on that run, for a human to `npm publish` by hand. Electron is **not** WIP on a branch anymore: the old root `AGENTS.md` \"Active issues\" section describing `smonga/electron_upgrade` as in-progress is gone entirely as of this checkout — `bindings/electron/` is a fully merged, real SDK on `main`. Re-check that section is still absent before trusting this note.\n\n`.github/workflows/electron-native-package.yml` itself is unchanged in what it builds: every backend for real (llamacpp/onnx/sherpa/neurt on their real platforms, qhexrt on a self-hosted Windows ARM64 runner) with the real `STAGING_BASE_URL` baked in, gated by `check_plugin_natives.py` (which fails closed on the staging-URL placeholder — the exact bug that shipped through `0.20.24`). What's new is that it's also invocable via `workflow_call`, and that `native_win_x64_and_package` now tolerates a failed/skipped QHexRT lane (self-hosted, single machine) rather than losing the other five real packages to one runner's outage. **`release.yml` itself still does not `npm publish` anything** — it only produces artifacts (Release assets for the public five, a workflow artifact for QHexRT) — same \"build automated, publish manual\" shape as every other platform. See `thoughts/shared/plans/electron_cicd_staging_url_and_npu_ane_pipeline.md` for the full history.\n\n**PROVEN END TO END on v0.20.25 (2026-08-22)** — the first release where this actually ran. Final state: 65 release assets, all 5 public Electron tarballs present with `.sha256` sidecars, zero `qhexrt`-named assets. Verified by downloading the *published* tarballs and running the repo's own gate against them:\n\n```bash\ngh release download v<version> --repo RunanywhereAI/runanywhere-sdks -p 'runanywhere-electron*.tgz' -D /tmp/verify\npython3 scripts/validation/gates/check_plugin_natives.py --expect-version <version> \\\n    $(for t in /tmp/verify/*.tgz; do printf -- '--tarball %s ' \"$t\"; done)\n# => All 20 check(s) passed: plugin natives are routable and no staging-URL placeholder was found.\n```\n\nThat single command asserts all three things you actually care about, per binary: the real `STAGING_BASE_URL` is baked in (no `YOUR_STAGING_BASE_URL`), `rac_commons` embeds the right version, and every backend is genuinely **routable** (`g_llamacpp_ops`, `g_sherpa_stt_ops`, `g_neurt_llm_ops`, `g_onnx_embeddings_ops`, …) rather than a same-sized shell. **Run it before every Electron `npm publish`** — it is the cheapest defense against exactly the class of defect that shipped `electron-sherpa` 0.20.17's NULL-vtable carrier. Expected shipped layout: entry package = `darwin-arm64` + `win32-x64` + `win32-arm64`; llamacpp/onnx/sherpa = `darwin-arm64` + `win32-x64`; neurt = `darwin-arm64` only; qhexrt = `win32-arm64` only.\n\nWhere to get the QHexRT tarball for its manual publish (it is deliberately NOT a release asset):\n\n```bash\ngh run download <release-run-id> --repo RunanywhereAI/runanywhere-sdks \\\n    --name sdk-electron-qhexrt-private --dir /tmp/qhexrt\n```\n\nIf asked to actually publish an Electron package (still a real, separate, manual step — this skill's general discipline in §0/§General verification still applies in full):\n- Involve the Windows box for real Windows-native verification before publishing anything Windows-targeted. The team keeps a **Windows ARM64 machine reachable over SSH**; its hostname, user, key, and mesh-VPN node name are operator-specific and deliberately not recorded in this repo — read the Windows/ARM64 host entry out of the operator's `~/.ssh/config` and use that alias. If there is no such entry, the box is not configured for you: say so and ask, rather than guessing at a hostname. Check reachability **first** by asking the mesh-VPN client whether the node is up (e.g. `tailscale status`, matching on the host entry's `HostName`) — if it reports `offline, last seen ...`, the box is asleep, which is not a configuration problem: ask the human to wake it, do not debug it as broken. Only then `ssh -o ConnectTimeout=8 \"$WIN_BOX\" \"whoami\"`. That same machine is **also** registered as a GitHub Actions self-hosted runner (labels `[self-hosted, Windows, ARM64, qhexrt]`) running as a Windows service — a queued `native_win_arm64_qhexrt` job on it means the machine is busy; check `gh api repos/RunanywhereAI/runanywhere-sdks/actions/runners` for the matching runner's `busy`/`status` before assuming it's free for manual SSH work, and vice versa.\n- **`@runanywhere/electron-qhexrt` being public on npm is a CONFIRMED, settled policy decision** (repo owner, 2026-08-20) — it contains real Hexagon NPU binaries (`libQnnHtpV81Skel.so`, `QnnHtp*.dll`, `rac_commons.dll`, `runanywhere_qhexrt.dll`) on purpose. The QHexRT-stays-private policy enforced everywhere else in this skill is specifically about the **GitHub-Release-side** asset set (Android etc.), a different channel — not a contradiction. Stop treating this as an unresolved item to flag; only re-raise it if you find evidence the policy changed.\n\n## 7. Final sanity pass\n\nAfter every registry is done, re-view each package's public listing/API and compare against the previous version — a big unexplained drop in file count or package size is a red flag to investigate before calling the release fully published:\n\n```bash\n# npm\nfor p in proto-ts web web-llamacpp web-onnx core llamacpp mlx onnx; do\n  npm pack @runanywhere/$p@$VERSION --dry-run 2>&1 | tail -5\ndone\n\n# Maven — check module metadata / POM sizes are non-trivial, not near-empty\nfor a in runanywhere-sdk runanywhere-llamacpp runanywhere-onnx; do\n  curl -s -o /dev/null -w \"%{size_download} bytes  $a\\n\" \\\n    \"https://repo1.maven.org/maven2/io/github/sanchitmonga22/$a/$VERSION/$a-$VERSION.aar\"\ndone\n\n# pub.dev\nfor p in runanywhere runanywhere_llamacpp runanywhere_onnx runanywhere_mlx; do\n  curl -s \"https://pub.dev/api/packages/$p\" | python3 -c \"import json,sys;d=json.load(sys.stdin);print('$p', d['latest']['version'])\"\ndone\n```\n\nOnly after this pass — every registry shows `$VERSION`, every artifact's contents check out, and nothing regressed in size/file-count versus the previous release — is the release fully published. Do not skip this step just because each individual publish call reported success; a successful upload is not the same claim as \"the right bytes are now live.\"\n",".claude/skills/sdk-release/SKILL.md":"---\nname: sdk-release\ndescription: Cut a new RunAnywhere SDK release end-to-end in RunanywhereAI/runanywhere-sdks — version bump through a fully green, tagged, drafted-then-published GitHub Release. Stops BEFORE publishing to package registries (npm/Maven/pub.dev/SwiftPM dist) — that is the separate \"sdk-publish\" skill, invoked as the last step here.\n---\n\n# SDK Release (runanywhere-sdks)\n\nRepo: `~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks` (verify with `pwd`/`ls` —\nthis is a workspace subfolder, not the repo root of the whole Qualcomm/ tree, and paths\ndrift; re-check script paths below still exist before trusting them blindly).\n\nThis is a **runbook**, not a description. Follow the steps in order. Every `gh`/`git`\ncommand below is meant to be run close to verbatim — adjust only the version/PR/run-id\nvalues for the release you're actually cutting.\n\n**THIS SKILL HAS BEEN SKIPPED THREE TIMES IN A ROW (v0.20.29, v0.20.30, v0.20.31),\nproducing the IDENTICAL failure each time, DESPITE step 2/3 below documenting the exact\nfix since the v0.20.25/v0.20.29 incidents.** All three times, an agent ran\n`sync-versions.sh`, opened the PR, merged it, and pushed the tag directly — skipping the\n\"dispatch a release candidate BEFORE merging, sync checksums from it, THEN merge\" sequence\nin steps 2-3 — usually because the version bump was one step inside a longer autonomous\nchain (a downstream repo re-pin, a multi-repo release cascade) and felt like \"just a\nversion bump,\" not a release. **A version bump to `core/VERSION` IS a release. If you are\nabout to run `scripts/release/sync-versions.sh` for ANY reason — including re-pinning this\nrepo as a step in some OTHER repo's release — stop and follow this skill from step 1,\nincluding the candidate dispatch. Do not improvise the sequence from memory even if you\ndid it correctly last time; do not skip it because \"this bump is small\" or \"we're in a\nhurry.\"** v0.20.31 additionally caught a second, previously-undetected instance of the\nsame class of bug in the SAME commit: `RunAnywhereMLXRuntime` in the Flutter podspec (which\n`RAC_CHECKSUM_SKIP` exempts from CI verification, so it fails silently for a consumer\ninstead of failing `publish`) was ALSO stale, and had to be independently re-verified\nagainst the same candidate build — see step 3's checksum-location table; check every row\nin it, not just the one CI complained about.\n\nCompanion skill: **sdk-publish** — invoke it at the very end (step 10) for the actual\nnpm / Maven / pub.dev / SwiftPM-distribution publishing. This skill's job stops at a\npublished (non-draft) GitHub Release with correct assets.\n\n---\n\n## 0. Orientation facts (confirm these still hold before relying on them)\n\n- Canonical version file: `core/VERSION` (single line, e.g. `0.20.24`). `core/VERSIONS`\n  also carries a `PROJECT_VERSION=` line that must agree — `sync-versions.sh` updates\n  both, `auto-tag.yml` reads `core/VERSIONS`, `check_release_version_coherence.sh` reads\n  `core/VERSION`. Don't hand-edit either; always go through the script.\n- `scripts/release/sync-versions.sh <version>` — bumps ~20 files (Package.swift,\n  gradle.properties, all package.json/pubspec.yaml, Kotlin/Swift/TS version constants,\n  AGENTS.md). Accepts a leading `v` and strips it.\n- `scripts/validation/gates/check_release_version_coherence.sh` — the gate that enforces\n  \"PR changing VERSION must carry exactly one `release:patch|minor|major` label, and the\n  new version must be exactly base+1 for that bump type.\" Reads `PR_BASE_SHA` and\n  `PR_RELEASE_LABELS_JSON` env vars when run under CI; locally it only checks the\n  canonical-version-format part.\n- `.github/workflows/pr-build.yml` job **`centralization`** is what actually runs the\n  coherence gate in CI (`Release-train version coherence` step), alongside\n  `check_typescript_centralization.sh`, `check_flutter_centralization.sh`,\n  `check_gradle_centralization.sh`, `check_wasm_provenance_contract.sh`,\n  `check_swift_dist_repo_sync.sh`, `check_agents_claude_sync.sh`,\n  `check_no_hardcoded_defaults.sh`. This job's trigger types are\n  `[opened, synchronize, reopened, labeled, unlabeled]` — **adding the release label\n  after opening the PR fires a fresh run**, which is how the gate clears without a new\n  commit (see step 1).\n- `.github/workflows/release.yml` — the build/package/draft-release workflow. Triggers:\n  tag push `v*.*.*` OR `workflow_dispatch` (works on **any branch**, not just tags) with\n  inputs `version` (required), `validate_external_starters` (bool, default false),\n  `publish_from_run_id` (optional), `reuse_native_web_run_id` (optional).\n- `.github/workflows/auto-tag.yml` — fires on `pull_request: closed` to `main` when the\n  merged PR carries a `release:*` label. Verifies the version bump matches the label,\n  pushes the `v<version>` tag, and **explicitly dispatches a fresh `release.yml` run at\n  that tag** (plain `git push` of a tag with `GITHUB_TOKEN` does not auto-trigger\n  workflows, so it calls `gh workflow run release.yml --ref v<version> -f version=<version>`\n  itself). This is the redundant rebuild you must cancel — see step 7.\n- Branch protection on `main`: check\n  `gh api repos/RunanywhereAI/runanywhere-sdks/branches/main/protection` — 2 approvals +\n  code owner review required, but look for a `bypass_pull_request_allowances` (users/teams/apps)\n  list. If your `gh api user --jq .login` is on that list, `gh pr merge --squash --admin`\n  is a legitimate, pre-configured bypass — not a hack. If not on the list, stop and ask a\n  human to approve/merge.\n- Never let any `qhexrt`-named asset reach the public asset set. `release.yml`'s own\n  manifest-assertion step (`find release-flat -maxdepth 1 -type f -iname '*qhexrt*'`)\n  hard-fails the run if one is found — this is enforced, not just policy.\n- **Electron IS part of `release.yml` now** (as of `fix/electron-cicd-staging-url-npu-ane`,\n  2026-08-21) — this reverses the prior \"deliberately separate\" policy noted below by an\n  earlier pass; the repo owner explicitly asked for Electron to produce ready-to-publish\n  artifacts through the real release train like every other SDK. `native_electron`\n  invokes `.github/workflows/electron-native-package.yml` as a **reusable workflow**\n  (`workflow_call`) rather than duplicating its ~400 lines of hard-won native-build logic\n  inline; `sdk_electron` downloads the resulting `electron-packaged-tarballs` artifact,\n  runs `scripts/release/prepublish_check.py` on every tarball, computes checksums, and\n  splits public vs. QHexRT-private before anything reaches `release-flat`. `publish`\n  hard-gates on `sdk_electron` succeeding (same tier as `sdk_kotlin`/`sdk_web`/\n  `sdk_react_native`) but treats `native_electron`'s own result as advisory, because that\n  job's result folds in `native_win_arm64_qhexrt` — a single, personally-owned\n  self-hosted NPU runner whose outage should cost only `@runanywhere/electron-qhexrt`\n  for that release, not the whole SDK train (`native_win_x64_and_package`, the job that\n  actually produces the tarballs, tolerates a failed/skipped QHexRT lane and still packages\n  the other five real artifacts). Like every other SDK here, this still does **not**\n  `npm publish` anything — it only produces the tarballs as GitHub Release assets\n  (public ones) / a workflow artifact (the private QHexRT one) for a human to publish by\n  hand, matching the \"build automated, publish manual\" shape everywhere else.\n  **Electron is no longer WIP on a branch** — the old root `AGENTS.md`/`CLAUDE.md`\n  \"Active issues\" section describing `smonga/electron_upgrade` as in-progress is gone\n  entirely as of this checkout; `bindings/electron/` is a fully merged, real SDK on\n  `main` with its own `AGENTS.md`. Re-check that section is still absent before trusting\n  this note — if it has reappeared, something regressed.\n  See `thoughts/shared/plans/electron_cicd_staging_url_and_npu_ane_pipeline.md` for the\n  full history (this pipeline shipped the placeholder-URL bug three times before\n  `electron-native-package.yml` existed).\n  **QHexRT public/private boundary — CONFIRMED POLICY, not an open flag**: the repo\n  owner explicitly confirmed (2026-08-20) that `@runanywhere/electron-qhexrt` stays\n  public on npm intentionally (it already is, and is a real, working precedent worth\n  studying before touching QHexRT packaging elsewhere) — the QHexRT-stays-private\n  policy enforced everywhere else in this pipeline is specifically about the\n  **GitHub-Release-side** asset set (e.g. Android), a different distribution channel.\n  This is settled; stop re-flagging it as an unresolved contradiction. Only re-raise it\n  if you find evidence the policy has changed.\n\n---\n\n## 0b. NeuRT is a PINNED PREBUILT now — check this before anything else\n\n`native_ios` no longer compiles the private `neurun` repo. It downloads archives that\nrepo publishes, pinned by tag + SHA-256 in `core/VERSIONS`, the same shape as sherpa-onnx:\n\n```\nNEURUN_REPO=RunanywhereAI/neurun      # shared: one repo publishes BOTH engines\nNEURT_RELEASE_TAG=v<version>          # per-engine: a release may carry one lane only\nNEURT_RAC_ABI_VERSION=9\nNEURT_MACOS_ARM64_SHA256=…\nNEURT_IOS_ARM64_SHA256=…\nNEURT_IOS_ARM64_SIMULATOR_SHA256=…\n```\n\n`NEURUN_REPO` is shared because one neurun release carries both engines. The **tags stay\nper-engine** on purpose: that repo can cut a release for one lane only, so a given tag may\ncarry just one engine's assets.\n\n**Run the gate after re-pinning** — it verifies the pins against the release itself, so a\nmistyped or locally-computed value fails there instead of mid-build:\n\n```bash\nNEURUN_TOKEN=\"$(gh auth token)\" bash scripts/validation/gates/check_engine_prebuilt_pins.sh\n```\n\n### Local engine development — there is no source mode\n\nThe engine has no `NEURT_ROOT` path any more; nothing here compiles engine source. To iterate\non NeuRT, build a slice in the neurun checkout and point the engine at it — no release, no\ntag, no pin edit:\n\n```bash\n# in neurun\nNeuRT/tools/scripts/package-rac-dist.sh --sdk-root <this repo> --version 0.0.0-dev \\\n    --slice macos-arm64 --out /tmp/devdist\nmkdir -p /tmp/devslice && tar -xzf /tmp/devdist/neurt-macos-arm64-v0.0.0-dev.tar.gz \\\n    -C /tmp/devslice --strip-components=1\n\n# here\nNEURT_PREBUILT_ROOT=/tmp/devslice cmake -S . -B build/dev -G Ninja \\\n    -DRAC_BUILD_BACKENDS=ON -DRAC_BACKEND_NEURT=ON -DRAC_RUNTIME_COREML=ON\n# expect: Mode: PREBUILT (macos-arm64, ABI 9)\n```\n\n`NEURT_PREBUILT_ROOT` is a **single-slice** root, which is why\n`build-core-xcframework.sh` deliberately ignores it: packaging needs all three slices, so\nhonouring a single-slice override there would let one staged slice satisfy the guard and\nsilently package two non-routable stubs. Use an obviously-not-a-release version like\n`0.0.0-dev` so a dev slice can never be mistaken for a published one.\n\n**Why this changed, and why it matters to you specifically.** `engines/neurt/CMakeLists.txt`\nused to resolve a `NEURT_ROOT` checkout of that private repo and **regex-parse its root\n`CMakeLists.txt`** for a source list. Under that arrangement, commons widened\n`rac_llm_stream_callback_fn` with `tokens_in_delta`, the pinned neurun commit still passed\n4 arguments, and `native_ios` died with *\"too few arguments to function call, expected 5,\nhave 4\"* on **v0.20.15, v0.20.16 and v0.20.17**. Every packaging job needs `native_ios`,\nso all three shipped **iOS-only — 18 assets where a healthy release has 55+**. That is the\nsingle most expensive failure this runbook exists to prevent, and it came from this seam.\n\n**Before opening the release PR**, confirm the pin is current:\n\n```bash\ngrep -A5 '^NEURT_REPO' core/VERSIONS\ngh release view \"$(grep '^NEURT_RELEASE_TAG=' core/VERSIONS | cut -d= -f2)\" \\\n    --repo RunanywhereAI/neurun --json assets --jq '.assets[].name'\n```\n\nExpect six assets — three `neurt-<slice>-v<version>.tar.gz` plus their `.sha256` sidecars.\n\n**If the plugin ABI changed this cycle** (`RAC_PLUGIN_API_VERSION` in\n`core/include/rac/plugin/rac_plugin_entry.h`), the pinned archives are invalid and\n`native_ios` will fail at the download step. Cut a neurun release first — that repo's\n`release_sdk_archives` skill is the runbook — then re-pin both `NEURT_RELEASE_TAG` and\n`NEURT_RAC_ABI_VERSION` here. The downloader refuses to run if `NEURT_RAC_ABI_VERSION`\ndisagrees with the header, so this cannot be half-done.\n\nProve it locally before trusting a tag build:\n\n```bash\nNEURUN_TOKEN=\"$(gh auth token)\" bash scripts/build/download-neurt.sh --all\n```\n\nThat verifies each tarball's SHA-256 **and** that its `RECEIPT.json` names the same ABI\nversion this repo defines. Then check configure prints `Mode: PREBUILT (<slice>, ABI N)`.\n`SOURCE` means a local neurun checkout was picked up (prebuilt is meant to win, so the\ndownload did not land); `SHELL` means the engine is **not routable** and a release built\nnow would ship an engine that registers, claims Apple availability and serves nothing.\n\n`release.yml` treats the downloader's exit **3** (\"no token — degrade to the shell\") as\n**fatal**. That degrade is right for a fork PR and catastrophic for a release.\n\n### The complete engine matrix — who consumes what, and how\n\nThere are only **three fetch points in the entire repo**. Everything else consumes an\nsdks artifact, which is why a single fetch fix can repair five SDKs at once — and why a\nsingle missing fetch can silently break several.\n\n```\nneurun release (private)\n  ├─ native_ios         ──NeuRT x3 slices──►  RABackendNeuRT.xcframework\n  │                                              ├─ Swift  (Package.swift binary target)\n  │                                              ├─ Flutter / React Native (co-vendored\n  │                                              │   in the onnx package's podspec)\n  │                                              └─ rcli-macos (stages it, then GREPS\n  │                                                  `backends --json` for neurt)\n  ├─ native_android     ──QHexRT arm64-v8a──►  one build stages jniLibs into\n  │                                              Kotlin + React Native + Flutter\n  └─ electron           ──NeuRT macos-arm64 AND QHexRT win-arm64──►  its own natives\n```\n\n| target | NeuRT | QHexRT | note |\n|---|---|---|---|\n| Swift (iOS/macOS) | ✅ xcframework | — | NeuRT is Apple-only |\n| Flutter / React Native (Apple) | ✅ xcframework | — | co-vendored in the onnx package |\n| Flutter / React Native (Android) | — | ✅ jniLibs | from the same Android build as Kotlin |\n| Kotlin (Android) | — | ✅ jniLibs | arm64-v8a only; the only ABI with a Hexagon NPU |\n| Electron macOS | ✅ fetches | — | builds its own natives |\n| Electron win-arm64 | — | ✅ fetches | |\n| rcli macOS | ✅ xcframework | — | **best check in the tree** (below) |\n| rcli linux / windows-x64 | — | — | correct: NeuRT is Apple-only, QHexRT is win-arm64 |\n\n**`rcli-macos` is the strongest verification that exists here.** It greps\n`backends --json` for `\"name\":\"neurt\"` — and a non-routable shell *refuses\nregistration*, so that grep proves the engine is **routable**, not merely present. Copy\nthis pattern when adding an engine rather than checking for a file on disk.\n\n**Two silent-failure traps this matrix exists to prevent:**\n\n- **`RAC_BACKEND_<X>=ON` does not mean the engine works.** Without a payload the engine\n  builds as a non-routable shell and registration is refused — no build error. Android\n  shipped that way for months, and `rcli-macos-release` still sets\n  `RAC_BACKEND_NEURT: ON` regardless of whether a payload was fetched.\n- **`copy_if_exists` in `build-core-android.sh` silently no-ops** when a lib was never\n  built. That is the same guarded-copy pattern that shipped Windows with zero engines for\n  several releases. The release now asserts `librunanywhere_qhexrt.so` is really inside\n  the arm64-v8a archive.\n\n`scripts/validation/gates/check_engine_prebuilt_pins.sh` asserts all of the above plus\nthe pins, the ABI, header drift against the receipt, and (with a token) the release\ncontents. Run it before cutting a release; it is also wired into `pr-build` offline.\n\n### QHexRT is pinned the same way — check it too\n\nThe Hexagon-NPU engine crosses the identical boundary, published by the same neurun release:\n\n```\nQHEXRT_RELEASE_TAG=v<version>\nQHEXRT_ARM64_V8A_SHA256=…      QHEXRT_ARM64_V8A_RECEIPT=…\nQHEXRT_WIN_ARM64_SHA256=…      QHEXRT_WIN_ARM64_RECEIPT=…\n```\n\nTwo pins per ABI, not one: the tarball checksum **and** the payload's own build-receipt\nhash. The receipt is the directory name under `engines/qhexrt/prebuilt/versions/`, so\npinning it is what makes the pin describe the bytes rather than trusting the archive's\nown claim about itself.\n\n```bash\nNEURUN_TOKEN=\"$(gh auth token)\" bash scripts/build/download-qhexrt.sh --abi arm64-v8a\nNEURUN_TOKEN=\"$(gh auth token)\" bash scripts/build/download-qhexrt.sh --abi win-arm64\n```\n\n**`current` selects ONE ABI at a time.** Both payloads can sit in `versions/` together, but\na build targets one platform — so re-run the downloader when switching between an Android\nand a Windows build. The downloader ends by running `validate-qhexrt-prebuilt.py` itself,\nso a bad payload fails at the pin rather than mid-build.\n\n**The QHexRT lane in neurun is deliberately non-blocking.** It runs on a single\nself-hosted Snapdragon box (QAIRT is licence-gated and exists on no hosted runner). If\nthat machine is offline, neurun's release publishes the Apple artifacts and **warns** that\nno QHexRT payload was produced — and the SDK simply keeps its previously pinned prebuilt.\nThat is correct behaviour, not a failure: do not block an SDK release on it. Only treat it\nas blocking if this release is specifically meant to ship new NPU kernels.\n\n**Do not hand-stage `engines/qhexrt/prebuilt/`.** That was the old flow, and it is how the\nElectron win-arm64 lane once pinned a receipt staged months earlier and silently shipped\nNPU kernels from an old commit. If the tree looks stale, re-pin and re-download; never\ncopy a payload in by hand.\n\n---\n\n## 1. Decide the version and open the release PR\n\nCheck for an already-open release PR first — don't duplicate work:\n\n```bash\ngh pr list --repo RunanywhereAI/runanywhere-sdks --label release:patch --label release:minor\n```\n\n(gh ORs multiple `--label` flags into a single AND-filtered query per invocation is not\nguaranteed across all gh versions — if unsure, run the two label queries separately:\n`--label release:patch` then `--label release:minor`, plus `--label release:major`.)\n\nPick a patch/minor/major bump based on the changes since the last tag. Then:\n\n```bash\ngit checkout -b release/v<version>\n./scripts/release/sync-versions.sh <version>      # e.g. 0.20.25 (no 'v' needed, either works)\nbash scripts/validation/gates/check_release_version_coherence.sh\n```\n\nThe coherence script may point out generated locks/changelogs that need regenerating\n(e.g. package-lock.json version bumps it expects but didn't find, or a missing CHANGELOG\nheading for the new version). Fix everything it flags, and add **explicit, human-written\nchangelog entries** — the gate requires a heading for the new version to exist; it does\nnot write prose for you.\n\nOpen the PR and **apply exactly one `release:patch` or `release:minor` (or `release:major`)\nlabel** — this is required, not optional. Two failure modes to know:\n\n- If you forget the label and push a commit later, the `centralization` job's\n  `Release-train version coherence` step fails.\n- **Adding the label after the PR is already open does NOT get picked up by the run that\n  already executed.** Because `pr-build.yml`'s `centralization` job triggers on\n  `labeled`/`unlabeled` PR events, applying the label fires a **new** run of the whole\n  job — that new run is what goes green. Don't push an empty commit to \"retrigger\" it;\n  just add/re-add the label (or use `gh pr edit <pr> --add-label release:patch`, and if it\n  was already present, remove + re-add to force the event).\n\n---\n\n## 2. Get REAL native-artifact checksums BEFORE merging — dispatch a release candidate\n\nDo not merge on faith that native builds will succeed. `release.yml`'s\n`workflow_dispatch` trigger works on **any branch**, including your unmerged release\nbranch — use it to build a full release candidate first:\n\n```bash\ngh workflow run release.yml --ref release/v<version> -f version=<version>\n```\n\nGet the run id and watch it:\n\n```bash\ngh run list --repo RunanywhereAI/runanywhere-sdks --workflow=release.yml --limit 5\n```\n\n**Do not block synchronously waiting on it.** Load the `Monitor` tool (or a background\npoll loop) to watch for completion/failure, and keep doing other release prep (changelog\nreview, CodeRabbit thread triage, branch-protection check) in the meantime. Keep\nnotifications to real state changes only — don't ping on every intermediate job\ncompleting, only on the run's overall failure or full completion.\n\n### REAL job durations — measured, not guessed. NEVER cancel a job for being \"slow\".\n\nMeasured on the 0.20.25 train (2026-08-21/22). A release train legitimately takes\n**4-5 hours** wall-clock when `native_web` has to build.\n\n| Job | Real duration | Notes |\n|---|---|---|\n| **`native_web`** (release.yml) | **~3h51m** | The long pole, by far. Its `Vendor ONNX Runtime and Sherpa-ONNX WASM archives` step alone is **~71 minutes** — it BUILDS onnxruntime + sherpa to WASM from source; it is not a git clone. Then 4 more WASM builds: CPU core+llamacpp+onnx ~89m, WebGPU llamacpp ~37m, WebGPU onnx+sherpa ~33m. |\n| `native_ios` | ~60-75 min | XCFrameworks + MLX. No longer compiles neurun — it downloads the pinned NeuRT archives (see §0b), which is seconds, not minutes. |\n| `swift-spm` (pr-build) | **69-85 min** | `Build XCFramework` step is most of it. Measured across 6 consecutive `main` runs (2026-08-21/22): 69, 78, 75, 85, 77, 69. A run sitting at ~78 minutes is **healthy**, not hung — check this row before reaching for cancel. |\n| `kotlin-android` (pr-build) | ~40 min | |\n| `python-windows` (pr-build) | ~40 min | |\n| `electron (windows-2022)` (electron-sdk-ci) | ~40 min | |\n| `rcli-windows` | ~30 min | |\n| `native_electron / native_win_arm64_qhexrt` | ~28 min | Self-hosted; may sit **queued** for a long time first if the single runner is busy. Queued ≠ hung. |\n| `native_electron / native_win_x64_and_package` | ~35-45 min | |\n| `wasm` (pr-build) | ~12 min | **Do NOT use this as a proxy for `native_web`.** Totally different scope. |\n\n**The mistake this table exists to prevent**: `pr-build.yml`'s `wasm` job takes ~12\nminutes, so a ~50-minute `native_web` \"vendor\" step looks hung. It is not. On the 0.20.25\ntrain this misread caused two healthy `native_web` jobs to be cancelled at 2h19m and\n~55m, wasting ~3 hours and two full candidate builds. Before concluding \"hung\", check\nstep-level progress and compare against THIS table:\n\n```bash\ngh run view <run-id> --repo RunanywhereAI/runanywhere-sdks --json jobs \\\n    --jq '.jobs[] | select(.name==\"native_web\") | {startedAt, steps: [.steps[] | select(.status==\"in_progress\") | .name]}'\n```\n\n### `reuse_native_web_run_id` is the correct way to avoid re-paying that ~4 hours\n\nIf a run needs redoing and **nothing under `bindings/web/wasm/`, `engines/`, or `core/`\nchanged** since a run whose `native_web` succeeded, reuse it instead of rebuilding:\n\n```bash\ngit diff --stat <good-run-sha> <current-sha>      # prove the scope first\ngh workflow run release.yml --ref release/<version> \\\n    -f version=<version> -f reuse_native_web_run_id=<good-run-id>\n```\n\n`native_web` then shows `skipped`, and `sdk_web` + `validate_consumer_web` still run and\npass against the reused artifact — which is also your proof the reuse was valid. Track the\nrun id that **originally built** it: that is the id you keep citing (see the chaining note\nin step 7).\n\n---\n\n## 3. Sync checksums once native_ios finishes\n\nPre-tag, `gh release download` will not work (there is no release yet) — pull the run's\nartifact directly:\n\n```bash\ngh run download <candidate-run-id> --repo RunanywhereAI/runanywhere-sdks \\\n    --name native-ios-macos --dir release-artifacts/native-ios-macos\n```\n\nVerify every `.zip` against its `.sha256` sidecar **yourself** before trusting it:\n\n```bash\ncd release-artifacts/native-ios-macos\nfor f in *.zip; do shasum -a 256 -c \"${f}.sha256\" || echo \"MISMATCH: $f\"; done\ncd -\n```\n\nThen sync:\n\n```bash\nbindings/swift/scripts/sync-checksums.sh release-artifacts/native-ios-macos\n```\n\n**KNOWN FACT — do not treat this as a bug**: `RunAnywhereMLXRuntime` (and occasionally\n`RACommons`) are **not byte-reproducible** between rebuilds of identical source. Expect\ntheir checksums to differ from any prior candidate build every single time you rebuild,\neven with zero source changes. Only investigate if a checksum for a framework that\nhistorically IS reproducible (e.g. plain XCFrameworks with no Swift codegen step)\nsuddenly changes.\n\n**Where each checksum actually lives — these are NOT all in one file:**\n\n| Manifest | Carries |\n|---|---|\n| root `Package.swift` | `RACommons` + the 5 backend XCFrameworks (6 checksums total) |\n| nested Flutter `Package.swift` (under `bindings/flutter/`) | same 6 |\n| `bindings/flutter/packages/runanywhere_mlx/ios/runanywhere_mlx.podspec` → `mlx_checksums` hash | `RunAnywhereMLXRuntime`, `RunAnywhereMLXMetal`, `RunAnywhereMLXResources` — **only here**, nowhere else |\n\nAfter `sync-checksums.sh` runs, verify centralization didn't drift and re-run the version\ncoherence gate locally:\n\n```bash\nbash scripts/validation/gates/check_flutter_centralization.sh\nbash scripts/validation/gates/check_release_version_coherence.sh\n```\n\nCommit and push the checksum-sync commit to the release branch:\n\n```bash\ngit add Package.swift bindings/flutter/Package.swift \\\n    bindings/flutter/packages/runanywhere_mlx/ios/runanywhere_mlx.podspec\ngit commit -m \"release: sync v<version> checksums from candidate run <candidate-run-id>\"\ngit push\n```\n\n### THIS STEP IS NOT OPTIONAL — skipping it fails `publish` after the tag already exists\n\n**What happens if you skip it** (observed on the 0.20.25 train): every build and package\njob goes green, `publish` gets all the way past flatten and the asset-manifest assertion,\nthen dies at **`Verify built Apple checksums match tagged package contracts`**:\n\n```\nmismatch: RACommonsBinary\n  Package.swift: 0609dae6...      release zip: 1ae55ae4...\nmismatch: RABackendLlamaCPPBinary\n  Package.swift: 0597442f...      release zip: 7062f7c8...\n>> Done. 8 processed, 0 missing, 4 failed.\nERROR: built Swift archives do not match the immutable tagged manifest\n```\n\nNote `8 processed / 4 failed` is really **2 distinct archives counted twice** (root\nmanifest + Flutter manifest each). On that train `RACommons` and `RABackendLLAMACPP`\ndrifted while ONNX/Sherpa/NeuRT/MLX/MLXMetal/MLXResources all matched — so \"6 of 8 passed\"\nis a normal-looking partial failure, not evidence of something exotic.\n\n**Recovery once the tag already exists** (this is safe and was done on 0.20.25):\n\n1. Confirm nothing was actually published — a failed `publish` dies *before* creating the\n   release, so there is usually nothing to undo:\n   ```bash\n   gh release view v<version> --repo RunanywhereAI/runanywhere-sdks   # expect \"release not found\"\n   ```\n2. Download the **exact artifacts the release will publish** (the candidate run named in\n   `publish_from_run_id`), verify each against its sidecar, and sync on a branch off `main`.\n3. Merge that branch, then **re-point the tag** at the new commit. `git push origin --delete`\n   can hang on flaky networks; the API is more reliable:\n   ```bash\n   gh api -X DELETE repos/RunanywhereAI/runanywhere-sdks/git/refs/tags/v<version>\n   gh api -X POST   repos/RunanywhereAI/runanywhere-sdks/git/refs \\\n       -f ref=\"refs/tags/v<version>\" -f sha=\"<new-main-sha>\"\n   ```\n   Only do this while **no release exists and nothing has been published**. If a release\n   was already published, do NOT move the tag — cut the next patch version instead.\n4. **Creating a tag via the API DOES trigger `release.yml`'s `push` trigger** (unlike a\n   `GITHUB_TOKEN` git-push of a tag, which does not). So immediately after re-tagging you\n   will have TWO runs: a full rebuild from the `push` event, and your own\n   `workflow_dispatch`. Identify by event and cancel the `push` one:\n   ```bash\n   gh run list --repo RunanywhereAI/runanywhere-sdks --workflow=release.yml --limit 3 \\\n       --json databaseId,event,headBranch --jq '.[] | \"\\(.databaseId) \\(.event) \\(.headBranch)\"'\n   gh api -X POST repos/RunanywhereAI/runanywhere-sdks/actions/runs/<push-run-id>/cancel\n   ```\n\nCheapest prevention: run `sync-checksums.sh --check <zip_dir>` against the candidate's\nartifacts and require **`N processed, 0 missing, 0 failed`** before you merge the release\nPR at all.\n\n**Recurred on v0.20.30 and again on v0.20.31** — same `mismatch: RACommonsBinary` /\n`mismatch: RACommons` failure, same root cause (step 2's candidate dispatch skipped\nentirely both times), fixed the same way both times: compute the real checksum from the\nalready-built release-artifact zip (`gh run download <failed-run-id> --name\nnative-ios-macos`), commit it on a fix branch, merge, then move the tag (§10d) to\nre-trigger `release.yml`. On v0.20.31 this ALSO surfaced the `RunAnywhereMLXRuntime`\npodspec entry as independently stale — `RAC_CHECKSUM_SKIP` means `publish` never checks\nit, so it would have shipped broken to Flutter consumers silently. **When fixing this\nfailure after the fact, check every row of the checksum-location table in step 3, not\nonly the archive named in the CI error** — the ones `RAC_CHECKSUM_SKIP` exempts are\nexactly the ones that won't tell you.\n\n---\n\n## 4. Watch for the \"package intentionally doesn't bundle a sibling dep\" bug class\n\nThis exact release hit a real bug in `bindings/web/scripts/package-sdk.sh`:\n`@runanywhere/web-onnx` deliberately does **not** bundle its own `proto-ts` copy (only\n`core`/`llamacpp` do — enforced by `bindings/web/scripts/validate_public_packages.py` and\nmirrored in `bindings/react-native/scripts/validate_public_packages.py`), but the\npackaging script's install-smoke-test (`verify_candidate_install`, around line 165 of\n`package-sdk.sh`) never supplied a resolution target for that unbundled dependency, so\n`npm install` reached out to the real npm registry for a not-yet-published version and\nfailed with `ETARGET`.\n\nFix pattern (already applied, keep it this way): the `onnx` call to\n`verify_candidate_install` passes the just-built local `proto-ts` tarball\n(`$PROTO_ARCHIVE`, built via `npm pack ../proto-ts`) as an explicit extra install arg —\nsee lines ~209-233 of `bindings/web/scripts/package-sdk.sh`, where `core` and `llamacpp`\ncalls omit it (they bundle proto-ts themselves) but `onnx`'s call appends `\"$PROTO_ARCHIVE\"`.\n\n**Generalize this as a standing check for every release**: grep for the asymmetric\nbundling flags before trusting any packaging script —\n\n```bash\ngrep -n \"expects_bundled_proto\\|--bundle\" \\\n    bindings/web/scripts/package-sdk.sh bindings/react-native/scripts/package-sdk.sh\n```\n\nWhenever a package intentionally does NOT bundle a sibling first-party dependency, its\nlocal pre-publish verification step must supply that dependency some other way (a local\ntarball) — the real registry version genuinely does not exist yet at candidate-build\ntime, and letting `npm`/`yarn` hit the real registry will `ETARGET`.\n\n**Before trusting any packaging script is bug-free, actually run it** against the real\ndownloaded native artifacts and check the exit code — do not just read the code and\nassume it's fine:\n\n```bash\ncd bindings/web && ./scripts/package-sdk.sh   # then check $?\ncd bindings/react-native && ./scripts/package-sdk.sh\n```\n\n### 4b. The two Electron-specific release bugs (both fixed — know the shapes)\n\nThe 0.20.25 train was the **first** release to run Electron packaging inside `release.yml`,\nand it surfaced two defects that no local test or green PR check could have caught. Both\nare fixed; recognize the shapes because they generalize.\n\n**(1) `sync-versions.sh` has a hardcoded package list, and new packages get missed.**\n`bindings/electron/packages/neurt/package.json` sat at the previous version while every\nsibling moved, because `neurt` was added (electron#734) after that list was last touched.\nSymptom: `native_win_x64_and_package` fails with\n\n```\nERROR: npm pack did not produce .../runanywhere-electron-neurt-<version>.tgz\n```\n\n— because `npm pack` correctly produced `...-neurt-<OLD-version>.tgz` from the stale\nmanifest. **Whenever you add a package anywhere, add it to `sync-versions.sh` the same\nday.** Cheap pre-flight check that catches the whole class:\n\n```bash\ngrep -h '\"version\"' bindings/electron/package.json bindings/electron/packages/*/package.json \\\n    bindings/electron/native/package.json | sort -u     # expect exactly ONE distinct version\n```\n\n**(2) `publish` downloads EVERY artifact, including intermediate hand-off bundles.**\n`publish`'s `Download all artifacts` step has no name filter. `electron-packaged-tarballs`\nis `native_win_x64_and_package`'s raw output whose only purpose is feeding `sdk_electron`,\nwhich re-uploads the curated public set as `sdk-electron`. Flattening both duplicated all\n5 Electron tarballs *and* dragged in the raw bundle's own `proto-ts` copy, which collided\nwith `sdk_web`'s:\n\n```\n::error::flatten collision: release-flat/runanywhere-proto-ts-<v>.tgz already exists\n::error::Refusing to publish — 6 filename collision(s) detected during flatten\n```\n\nFixed by pruning that directory in the flatten `find`\n(`-type d -name \"electron-packaged-tarballs\" -prune -o -type f \\( ... \\)`). **Standing\nrule: any new intermediate artifact must be either excluded from the flatten or named so\nit cannot collide.** When editing that `find`, test the exact expression against a fake\n`release-artifacts/` tree locally first — `-prune -o` + `-print0` is easy to get subtly\nwrong and the only other way to find out costs a full release run.\n\n---\n\n## 5. Resolve CodeRabbit (or any bot) review findings properly\n\nDo not resolve a review thread without a real, evidence-backed reply. Steps:\n\n1. Fetch unresolved threads via GraphQL:\n\n```bash\ngh api graphql -f query='\nquery($owner:String!, $repo:String!, $pr:Int!) {\n  repository(owner:$owner, name:$repo) {\n    pullRequest(number:$pr) {\n      reviewThreads(first:100) {\n        nodes { id isResolved comments(first:10) { nodes { id databaseId body } } }\n      }\n    }\n  }\n}' -f owner=RunanywhereAI -f repo=runanywhere-sdks -F pr=<pr-number>\n```\n\n2. For each unresolved thread, reply via REST with `in_reply_to` set to the comment's\n   `databaseId` (not the GraphQL node id), citing concrete evidence — a real run id,\n   a real checksum, a real commit SHA, not \"looks fine\":\n\n```bash\ngh api -X POST repos/RunanywhereAI/runanywhere-sdks/pulls/<pr-number>/comments \\\n    -f body=\"Verified against candidate run <run-id>: RunAnywhereMLXRuntime checksum\nmatches release-artifacts/native-ios-macos/RunAnywhereMLXRuntime-ios-v<version>.zip.sha256.\nSee commit <sha>.\" \\\n    -F in_reply_to=<comment-database-id>\n```\n\n3. Only then resolve, via GraphQL mutation on the thread id from step 1:\n\n```bash\ngh api graphql -f query='\nmutation($threadId:ID!) { resolveReviewThread(input:{threadId:$threadId}) { thread { isResolved } } }\n' -f threadId=<thread-node-id>\n```\n\n---\n\n## 6. Merge\n\nBefore merging, verify:\n\n```bash\ngh pr view <pr-number> --repo RunanywhereAI/runanywhere-sdks --json mergeable,reviewDecision\n```\n\nZero unresolved review threads (re-check the GraphQL query from step 5 — it should return\nnone with `isResolved: false`), and `mergeable == \"MERGEABLE\"`.\n\nConfirm bypass eligibility before relying on `--admin`:\n\n```bash\ngh api repos/RunanywhereAI/runanywhere-sdks/branches/main/protection \\\n    --jq '.required_pull_request_reviews.bypass_pull_request_allowances'\ngh api user --jq .login\n```\n\nIf your login appears in that allowance list (users, or a team/app you belong to):\n\n```bash\ngh pr merge <pr-number> --repo RunanywhereAI/runanywhere-sdks --squash --admin\n```\n\nIf not on the list, **stop** — ask a human to approve or merge instead of forcing it.\n`--admin` is a legitimate configured bypass only for allow-listed identities; do not use\nit to route around review for anyone else.\n\n---\n\n## 7. GOTCHA — cancel the auto-tag-dispatched rebuild, dispatch a publish-only run instead\n\nThe moment the PR merges, `auto-tag.yml` runs, pushes the `v<version>` tag, and **itself**\ndispatches a brand-new full `release.yml` run at that tag (see orientation section — plain\ntag pushes via `GITHUB_TOKEN` don't auto-trigger, so the workflow does it explicitly).\nThis is redundant — you already have a fully green, checksum-verified candidate from step\n2-3 — and wasteful, because Apple/MLX bytes will drift again for zero benefit (see the\nnon-reproducibility fact in step 3).\n\n**Cancel it immediately, before it burns real compute** (ideally within the first minute\nor two, before native_ios starts real work):\n\n```bash\ngh run list --repo RunanywhereAI/runanywhere-sdks --workflow=release.yml --limit 5\ngh api -X POST repos/RunanywhereAI/runanywhere-sdks/actions/runs/<auto-triggered-run-id>/cancel\n```\n\nThen dispatch a **publish-only** run that reuses your already-verified candidate's exact\nbytes instead of rebuilding:\n\n```bash\ngh workflow run release.yml --ref v<version> \\\n    -f version=<version> \\\n    -f publish_from_run_id=<candidate-run-id>\n```\n\n**CRITICAL SUB-GOTCHA**: if your candidate run in step 2 itself passed\n`reuse_native_web_run_id=<X>` to skip rebuilding WASM (a packaging-only re-release), that\ncandidate run **never uploaded its own copy of the `native-web` artifact** — it was\nskipped. `publish_from_run_id` alone will then fail during the release-manifest assertion\nwith `\"Missing or empty Web WASM payload\"`, because the publish job's artifact-download\nstep only pulls `reuse_native_web_run_id`'s artifact when that input is passed to *this*\ndispatch too (see `.github/workflows/release.yml` around the \"Download reused Web natives\"\nstep — it is gated on `inputs.reuse_native_web_run_id != ''`, independent of\n`publish_from_run_id`). You must **also** pass the same original run id:\n\n```bash\ngh workflow run release.yml --ref v<version> \\\n    -f version=<version> \\\n    -f publish_from_run_id=<candidate-run-id> \\\n    -f reuse_native_web_run_id=<X>\n```\n\nThis chains forward across releases: whichever run **originally** built `native_web` is\nthe run you keep citing at every subsequent `reuse_native_web_run_id`, not the most recent\nrun that merely borrowed it from an earlier one. Track that original run id explicitly if\nyou're doing back-to-back packaging-only releases.\n\n---\n\n## 8. Verify the draft release's asset set\n\nThe publish job only creates a **draft** GitHub Release (`softprops/action-gh-release`,\n`draft: true`) with built assets attached — it does not touch npm/Maven/pub.dev at all\n(grep `release.yml` for `npm publish`, `mavenCentral`, `pub publish` — none exist there).\n\nExpected asset count for a full, non-Windows-focused release: **~65 assets** —\n\n- 9 Apple archives (iOS/macOS XCFrameworks: RACommons + 5 backends + MLX runtime/Metal/\n  resources + CoreML/NeuRT)\n- 3 Android RACommons zips (arm64-v8a, armeabi-v7a, x86_64)\n- 1 Linux RACommons tar.gz\n- 1 Windows RACommons zip (preserved as an asset, advisory — see step 9)\n- 1 Web WASM tar.gz (`RACommons-web-v<version>.tar.gz`)\n- 3 rcli tarballs (macOS arm64, Linux x86_64, Windows x86_64 — macOS may also have a\n  notarized `.dmg` if Developer ID/notary secrets are configured; this is soft-skipped\n  otherwise, not a failure)\n- 1 Kotlin Maven repo zip\n- 8 npm tarballs (proto-ts, web core, web-llamacpp, web-onnx, RN core, RN llamacpp, RN mlx,\n  RN onnx)\n- 5 Electron npm tarballs (`runanywhere-electron-<v>.tgz` core, `-llamacpp`, `-onnx`,\n  `-sherpa`, `-neurt`) — added by `sdk_electron`; each has its own `.sha256` sidecar\n  counted separately below. **`electron-qhexrt` is deliberately excluded** — it still\n  gets built and packaged, but only as the `sdk-electron-qhexrt-private` workflow\n  artifact (not a release asset); download it separately for a manual `npm publish`.\n- 1 `MANIFEST.txt`\n- **explicitly ZERO** `qhexrt`-named files (the manifest assertion step hard-fails the run\n  if it finds any — confirm the run actually reached the publish job and didn't die there)\n\nEvery asset above except `MANIFEST.txt` has a `.sha256` sidecar, which is itself a\nseparate asset. **v0.20.25 landed at exactly 65 assets** — use that as the concrete\nbaseline rather than re-deriving the arithmetic.\n\nCross-check every checksum's `.sha256` sidecar against what got committed to\n`Package.swift` / the flutter `Package.swift` / the MLX podspec in step 3 — they must\nmatch byte for byte, because this run's artifacts are exactly what step 10 (sdk-publish)\nships to real consumers.\n\n```bash\ngh run view <publish-run-id> --repo RunanywhereAI/runanywhere-sdks --json jobs \\\n    --jq '.jobs[] | {name, conclusion}'\n\n# Asset count + the Electron set + the qhexrt-leak assertion, in three commands:\ngh release view v<version> --repo RunanywhereAI/runanywhere-sdks --json assets --jq '.assets|length'\ngh release view v<version> --repo RunanywhereAI/runanywhere-sdks --json assets \\\n    --jq '[.assets[].name | select(startswith(\"runanywhere-electron\"))] | sort'\ngh release view v<version> --repo RunanywhereAI/runanywhere-sdks --json assets \\\n    --jq '[.assets[].name | select(test(\"qhexrt\";\"i\"))]'          # MUST be []\n```\n\n### Prove the artifacts are real — don't trust green checkmarks\n\nA green `publish` only proves the assertions ran. To prove the bytes are right, run the\nrepo's own fail-closed gate against the **published** tarballs. This is the single highest\n-value verification in the whole runbook and it takes about a minute:\n\n```bash\ngh release download v<version> --repo RunanywhereAI/runanywhere-sdks \\\n    -p 'runanywhere-electron*.tgz' -D /tmp/verify\npython3 scripts/validation/gates/check_plugin_natives.py --expect-version <version> \\\n    $(for t in /tmp/verify/*.tgz; do printf -- '--tarball %s ' \"$t\"; done)\n```\n\nExpect `All N check(s) passed: plugin natives are routable and no staging-URL placeholder\nwas found.` On v0.20.25 that was **20 checks** across 5 tarballs, asserting three separate\nthings per binary: the real `STAGING_BASE_URL` is baked in (no `YOUR_STAGING_BASE_URL`\nplaceholder), `rac_commons` embeds the right version string, and each backend is actually\n**routable** (real op tables — `g_llamacpp_ops`, `g_sherpa_stt_ops`, `g_neurt_llm_ops`,\netc.), which is what distinguishes a real plugin from a same-sized shell.\n\nSanity-check the shipped layout too — a tarball missing a platform is a silent regression:\n\n| Package | Expected `prebuilds/` |\n|---|---|\n| `@runanywhere/electron` | `darwin-arm64` + `win32-x64` + `win32-arm64` |\n| `-llamacpp`, `-onnx`, `-sherpa` | `darwin-arm64` + `win32-x64` |\n| `-neurt` | `darwin-arm64` only (Apple Neural Engine) |\n| `-qhexrt` (private artifact) | `win32-arm64` only (Hexagon NPU) |\n\n---\n\n## 9. Windows handling — build/preserve only, never auto-publish\n\n`native_windows` and `native_rcli_windows` run and their outputs are **preserved** as\nrelease assets (proving the binaries build), but nothing Windows-specific is published to\nany package registry as part of the standard flow unless a human explicitly asks for it —\nthat's a deliberate, separate decision, not an oversight.\n\nFor anything that needs actual execution on Windows (smoke-testing the native Windows\nbuild, or Electron's Windows packaging), do not assume a macOS cross-build proves\nWindows correctness — SSH to the real Windows box. The team keeps a **Windows ARM64\nmachine reachable over SSH**; its hostname, user, key, and mesh-VPN node name are\noperator-specific and deliberately not recorded in this repo — read the Windows/ARM64\nhost entry out of the operator's `~/.ssh/config` and use that alias. If there is no such\nentry, the box is not configured for you: say so and ask, rather than guessing.\n\n```bash\nWIN_BOX=<the ssh alias from ~/.ssh/config>\n# Ask the mesh-VPN client whether the node is up FIRST (e.g. `tailscale status`,\n# matching on that host entry's HostName) — never just try SSH and retry on timeout.\nssh -o ConnectTimeout=8 \"$WIN_BOX\" \"whoami\"\n```\n\nIf the VPN reports the node offline/asleep, **that's not a config problem** — ask the\nuser to wake the box; don't debug SSH config against a machine that's simply off.\n\n---\n\n## 10. Publish the draft, then hand off\n\nOnce the draft's asset set is verified complete and correct (step 8):\n\n```bash\ngh release edit v<version> --repo RunanywhereAI/runanywhere-sdks --draft=false --latest\n```\n\nThis is the point where SwiftPM's `from: \"<version>\"` resolution and the download URLs\nbaked into `Package.swift` start actually working for real external consumers — draft\nrelease assets are not publicly downloadable, so nothing before this line is\nexternally live.\n\n**Hand off to the `sdk-publish` skill** for the actual npm / Maven Central / pub.dev /\nSwiftPM-distribution-repo publishing steps. This skill's job ends here.\n\n---\n\n## 10b. MANDATORY, and it turns `main` RED until you do it — cut runanywhere-swift\n\nThe moment `v<version>` exists in this repo, `check_swift_dist_repo_sync.sh` (run by\n`pr-build.yml`'s `centralization` job) **fails on every PR and every push to `main`**:\n\n```\n[FAIL] runanywhere-swift has no 0.20.25 tag (latest: 0.20.24)\n       v0.20.25 is published here, so 'from: \"0.20.25\"' is broken for every\n       SwiftPM consumer of https://github.com/RunanywhereAI/runanywhere-swift.git\n```\n\nThis is **by design**, not a bug, and it is the #1 reason a release looks \"not green\"\nafter everything else succeeded. Do not go hunting for a code defect — cut the repo:\n\n```bash\ngit clone https://github.com/RunanywhereAI/runanywhere-swift.git /tmp/ra-swift\nbindings/swift/scripts/sync-dist-repo.sh --zips <zip_dir> --tag /tmp/ra-swift\n# verify BEFORE pushing (content check passes locally; the tag check needs the push)\nRUNANYWHERE_SWIFT_DIST_REPO=/tmp/ra-swift \\\n    bash scripts/validation/gates/check_swift_dist_repo_sync.sh\ngit -C /tmp/ra-swift push origin main --follow-tags\nbash scripts/validation/gates/check_swift_dist_repo_sync.sh    # now [OK]\n```\n\nGotchas found doing this on 0.20.25:\n\n- `--check` **cannot be combined** with `--zips`/`--commit`/`--tag` (hard error). Run a\n  bare `--check <repo>` first to preview, then the real invocation separately.\n- `--check` legitimately reports `ERROR: distribution repo is out of sync` before you\n  sync — that's the preview telling you what it will change (expect the `sdkVersion` bump,\n  ~3 changed files, `0 added / 0 removed`). Non-zero exit there is not a failure.\n- The script **never pushes**; `--tag` only creates the tag locally. The dist repo tag has\n  **no `v` prefix** (`0.20.25`, not `v0.20.25`) while the monorepo tag does.\n- Running the gate locally right after `--tag` still shows `[FAIL] ... has no <version>\n  tag` while simultaneously showing `[OK] ... checkout matches: all 6 checksums`. That is\n  expected: the content check reads your local checkout, the tag check queries the real\n  remote. It clears the moment you push.\n- It chains into `sync-checksums.sh`, so run it **after** step 3's checksums are committed,\n  or the dist manifest gets stale hashes.\n\n---\n\n## 10c. Windows + self-hosted-runner traps (read before debugging a red Windows job)\n\nThese all cost a full CI cycle. The unifying fact:\n\n> The self-hosted runner service executes as `NT AUTHORITY\\NETWORK SERVICE`, which has its\n> own PATH **and** its own NTFS permissions. Anything you verify over SSH on that box tells\n> you nothing about what the job can do. `native_win_arm64_qhexrt` already carries a long\n> comment about this — read it before inventing a fix, and keep it in sync with neurun's\n> equivalent lane, which runs on the *same machine*.\n\n**Environment**\n\n- **`bash: command not found`** — Git for Windows puts `Git\\cmd` (git.exe) on PATH but not\n  `Git\\bin` (bash.exe). `actions/checkout` still works because it calls git.exe directly,\n  which makes this look unrelated to runner setup.\n- **`pwsh` does not exist on that box** — Windows PowerShell 5.1 only. Use\n  `shell: powershell`.\n- **`gh` is not installed on that box, and must not be assumed anywhere.** A QHexRT\n  download died on `gh: command not found`. The fix is deliberately *not* to install it:\n  every fetch of a private release asset now goes through\n  `fetch_release_asset()` in `scripts/build/_release_asset.sh`, which uses `curl` + the\n  REST API. `curl` ships with Windows, macOS and every Linux image we use, so this is one\n  code path everywhere instead of a per-runner provisioning step. If you add a new\n  consumer of a private asset, source that helper — do not reach for `gh`.\n  Note the helper matches the asset name **exactly**: a prefix match picks the `.sha256`\n  sidecar (its name starts with the tarball's), and you get a 65-byte \"archive\" that fails\n  much later with a confusing error.\n- **Tool availability is the single most common failure class on this runner** — `bash`,\n  `python3`, `ninja`, `gh`, GNU `tar` and NDK `.exe` suffixes have each broken a lane\n  once. When a self-hosted job fails, check \"does this tool exist as spelled\" before\n  reading any logic.\n- **`ln -s` under Git-Bash COPIES the directory** unless `MSYS=winsymlinks:nativestrict`.\n  It does not fail, so the result looks like real content and whatever validates a\n  \"must be a symlink\" invariant rejects a payload that was actually fine. Never use shell\n  `ln -s` on this box — go through `scripts/build/_selection.py`.\n- **Windows selects with a JUNCTION, not a symlink, and that is deliberate.** The runner\n  service account cannot create symlinks (below), but a junction is the same kind of\n  reparse point to a directory and needs no privilege. Two traps when reading that code:\n  `Path.is_symlink()` returns **False** for a junction (`stat.S_ISLNK` is true only for\n  `IO_REPARSE_TAG_SYMLINK`) — use `_selection.is_selection()`; and a junction stores an\n  **absolute** t","AGENTS.md":"# AGENTS.md\n\nGuidance for AI coding assistants working in this repository.\n\n> **`AGENTS.md` is the real file; each `CLAUDE.md` is a committed symlink to the\n> `AGENTS.md` beside it.** Editing either name edits the same bytes. If you create a new\n> `AGENTS.md` in a directory that doesn't have one, run\n> `bash scripts/validation/gates/check_agents_claude_sync.sh --fix` from the repo root to\n> create its `CLAUDE.md` symlink — `pr-build.yml` fails any tracked `AGENTS.md` missing one.\n> Never hand-create a `CLAUDE.md` as a real file.\n\n## Working conventions\n\n- **Focus on simplicity.** Clean, SOLID-ish separation of concerns; reuse over\n  reinvention. No mock implementations and no new unit tests unless the task explicitly\n  asks for them.\n- **Plan first for non-trivial work.** Write the plan to\n  `thoughts/shared/plans/{descriptive_name}.md`, get it approved before implementing, and\n  keep it updated (append what changed) as you go so the next engineer can pick it up.\n- **Structured types over raw strings.** Enums/sealed classes/data classes for\n  configuration and results — this is enforced per-SDK by lint (ktlint/detekt, SwiftLint,\n  ESLint) so ad hoc strings tend to get caught anyway.\n- **Read files fully** before editing them; only use offset/limit on files too large to\n  read at once.\n- **Use full local build parallelism** (`-j \"$(sysctl -n hw.logicalcpu)\"` / equivalent),\n  scaling down only under real memory/thermal pressure — not because load average looks\n  high.\n- **Multi-agent workflows run on Sonnet.** When fanning work out to subagents (the\n  `Workflow` tool, or several parallel `Agent` calls), pass `model: 'sonnet'` on every\n  `agent(...)` call rather than letting them inherit the session model. A fan-out of\n  8-10 probes reading large files is exactly the shape where Sonnet is the right\n  cost/latency trade, and these agents are doing bounded, well-specified retrieval and\n  analysis rather than open-ended reasoning. Reserve the session's stronger model for\n  the synthesis step that consumes their reports, and for anything where a wrong answer\n  is expensive and hard to detect — a final design decision, an adversarial verification\n  pass, or a security-relevant judgement.\n\n  ```js\n  // in a Workflow script\n  const findings = await parallel(LENSES.map((l) => () =>\n    agent(l.prompt, { label: `probe:${l.key}`, schema: FINDINGS, model: 'sonnet' })))\n\n  // the synthesis that reads them can stay on the session model\n  const plan = await agent(synthesisPrompt, { schema: PLAN, effort: 'high' })\n  ```\n\n## The most important architectural rule: logic lives at the lowest layer that serves everyone\n\nEach feature/modality (LLM, STT, TTS, VAD, VLM, RAG, LoRA, Voice) is invoked through\n**one** SDK entry point; the SDK, and below it C++ commons, does all the heavy lifting. If\nan example app builds a multi-step bootstrap sequence, hardcodes a model/engine constant,\nor post-processes model output, that is a bug in the SDK, not the app — fix it down a\nlayer.\n\n1. **C++ commons (`core/`)** — cross-platform, non-I/O logic: model lifecycle, registry,\n   download orchestration, RAG session management, inference routing. All 5 SDKs get the\n   fix for free.\n2. **Platform SDK layer** — platform-specific I/O or runtime bridging only (Web OPFS,\n   iOS Keychain, Android Keystore, WASM MEMFS mirroring).\n3. **Example apps** — UI rendering, navigation, thin SDK calls. No business logic, no\n   internal-SDK knowledge (path patterns, framework→directory mappings), no workarounds.\n\n**iOS is the source of truth.** When behavior is ambiguous in any other SDK, check the\niOS Swift implementation first and copy the logic exactly, adapting only syntax.\n\n---\n\n## Repository overview\n\nCross-platform on-device AI SDK monorepo. A single C/C++ core (`runanywhere-commons`,\n~118K first-party LOC plus ~420K generated proto bindings) implements all AI business\nlogic behind a pure C ABI (`rac_*` prefix). Platform SDKs are thin bridges that supply\nplatform services (file I/O, HTTP, Keychain, audio) via an inversion-of-control struct and\ncall into the C core for all inference. Protobuf IDL schemas generate type-safe bindings\nfor every language.\n\n**Current version**: `0.20.37` (canonical source: `core/VERSION`)\n\n| SDK | Path | Bridge mechanism | Platforms |\n|-----|------|-------------------|-----------|\n| Swift | `bindings/swift/` | XCFramework + CRACommons module map | iOS 17.5+, macOS 14.5+ |\n| Kotlin | `bindings/kotlin/` | JNI (`librunanywhere_jni.so`) | Android (min API 24) |\n| Flutter | `bindings/flutter/` | Dart FFI | iOS, Android |\n| React Native | `bindings/react-native/` | NitroModules (JSI HybridObject) | iOS 17.5+, Android arm64 |\n| Web | `bindings/web/` | Emscripten WASM + TypeScript | Chrome, Safari, Firefox |\n| Electron | `bindings/electron/` | Node-API addon | Windows x64 (preview) |\n| Python | `bindings/python/` | pybind11 extension (`runanywhere._core`) | Windows, macOS, Linux (alpha) |\n\nEach has its own `AGENTS.md` with full architecture, build commands, and conventions —\nread it before working in that SDK.\n\n| Native dir | Contents |\n|-----------|----------|\n| `core/` | C/C++ core library: all AI logic, plugin registry, event system |\n| `engines/` | 7 backend plugins: llamacpp, sherpa, onnx, cloud, mlx, qhexrt, neurt |\n| `runtimes/` | 3 runtime adapters: cpu (always), onnxrt, coreml |\n| `idl/` | 39 Protobuf schemas + per-language codegen scripts |\n\n### Consumer apps live in separate repos\n\nThe four full consumer apps were extracted into standalone repos (history preserved); PR\nagainst them there, not here: [runanywhere-ios](https://github.com/RunanywhereAI/runanywhere-ios),\n[runanywhere-android](https://github.com/RunanywhereAI/runanywhere-android),\n[runanywhere-web](https://github.com/RunanywhereAI/runanywhere-web),\n[runanywhere-electron](https://github.com/RunanywhereAI/runanywhere-electron).\n\nTwo example apps remain in-tree (`bindings/flutter/example/`, `bindings/react-native/example/`).\nAll example apps share brand orange `#FF6900`, documented in `docs/DESIGN_GUIDELINE.md`.\n\n### Minimal examples are how you verify an SDK change locally\n\nEach in-repo example consumes its SDK from local source (no staging/publishing needed to\nsee an edit): Swift via `RUNANYWHERE_USE_LOCAL_NATIVES=1`, Kotlin via a Gradle composite\nbuild (`includeBuild` + `dependencySubstitution`), Web via Vite aliases into\n`packages/*/src`. They're deliberately minimal (one prompt in, one streamed completion\nout) — feature-complete UI belongs in the consumer repos above.\n\n---\n\n## Cross-platform architecture\n\n`idl/*.proto` is the schema root; `idl/codegen/generate_all.sh` emits per-language\nbindings (Swift, Kotlin/Wire, TS/ts-proto, Dart), all generated and none committed — see\n[`docs/reference/generated-code-contract.md`](docs/reference/generated-code-contract.md)\nfor the full \"what generates what, when\" contract before touching `idl/`.\n\nAll SDKs reach `runanywhere-commons` through the `rac_*` C API. Commons holds the\ncomponent layer (lifecycle), the service layer (dispatch), and the plugin registry, and\nreaches engines through `rac_engine_vtable_t`.\n\n| Engine | Primitives |\n|---|---|\n| llamacpp | LLM, VLM |\n| sherpa-onnx | STT, TTS, VAD |\n| onnx | Embed, Segment |\n| qhexrt | Hexagon NPU |\n| neurt, cloud | Apple Neural Engine, HTTP |\n\nBackend base priorities (highest wins per primitive via `rac_plugin_find()`, no\nruntime/format scoring): qhexrt=150 (QNN-context models only), mlx=110 (Apple),\nllamacpp=100, sherpa=90, onnx/cloud=50. An explicit engine name is honored through\n`rac_plugin_find_for_engine()` regardless of priority.\n\n### Non-obvious invariants that span every SDK\n\n- **Platform adapter IoC**: `rac_platform_adapter_t` is a flat C struct of function\n  pointers populated by each SDK before `rac_init()`. C++ never calls platform APIs\n  directly — file I/O, HTTP, Keychain, logging, memory queries all pass through it.\n- **Two-phase init**: every SDK does Phase 1 (sync: register adapter, load native libs,\n  configure logging) then Phase 2 (async: authenticate, register device, fetch model\n  assignments, discover downloaded models).\n- **Plugin ABI**: `RAC_PLUGIN_API_VERSION = 9u`; a version mismatch is an immediate\n  rejection. Each vtable has 10 active primitive slots and 7 reserved. NULL slot = not\n  supported. `rerank_ops` (wire value 11) was promoted from `reserved_slot_2` in ABI v8;\n  wire value 6, retired in ABI v4, stays permanently retired — never reuse it. See\n  `core/AGENTS.md` for the full ABI history and how to add a primitive.\n- **Static vs. dynamic plugins**: iOS and WASM force `RAC_STATIC_PLUGINS=ON` (no\n  `dlopen`), registered via `RAC_STATIC_PLUGIN_REGISTER(name)` + `-force_load` /\n  `--whole-archive`. Android/Linux/macOS default to dynamic loading via\n  `rac_registry_load_plugin()`.\n- **Streaming fan-out**: C++ allows only one proto-byte callback per component handle.\n  Each SDK implements its own fan-out to multiplex that into multiple subscribers (Swift\n  `AsyncStream`, Kotlin `Flow`, Dart `StreamController`, TS `AsyncIterable`).\n- **Proto types are canonical**: never hand-write enum values or structured types that\n  exist in `idl/*.proto` — use the generated types/typealiases and regenerate instead.\n- **HTTP is platform-provided**: no libcurl. Each SDK registers a\n  `rac_http_transport_ops_t` (URLSession on Apple platforms, OkHttp on Android,\n  `emscripten_fetch` on Web).\n- **NeuRT crosses a repo boundary as BYTES, not source**: the Apple engine is built and\n  published by the private `neurun` repo and consumed here as prebuilt archives, pinned by\n  tag + SHA-256 in `core/VERSIONS` (`NEURT_*`). Fetch with `scripts/build/download-neurt.sh`.\n  Each archive carries a receipt naming the `RAC_PLUGIN_API_VERSION` it was built against,\n  and both the downloader and CMake refuse a mismatch — a vtable-layout change relinks\n  cleanly and corrupts dispatch at runtime, so nothing else would catch it.\n\nA side-by-side comparison of entry point / bridge / streaming / events / storage / HTTP\nper SDK — useful when porting a fix across SDKs — lives in\n[`docs/reference/cross-sdk-parity.md`](docs/reference/cross-sdk-parity.md).\n\n---\n\n## Building and running\n\nThe root `CMakeLists.txt` (version from `core/VERSION`) is the single entry point for\nnative builds; `CMakePresets.json` defines `macos-{debug,release}`, `linux-{debug,release,asan}`,\n`ios-{device,simulator}`, `android-arm64`, `wasm`, `cpp-desktop-*`, and the `windows-*` presets.\n\n```bash\ncmake --preset macos-debug && cmake --build build/macos-debug && ctest --preset macos-debug\n```\n\n**`./run <group> [subcommand]`** is the unified wrapper for everything else — build,\nlint, and run any SDK or example app from one CLI (`./run --help` for the full menu):\n\n```bash\n./run doctor                    # scan host toolchains, show what's buildable here\n./run setup                     # provision env for every host-buildable target\n./run sdk commons build-android # build C++ commons for all Android ABIs, stage .so files\n./run sdk kotlin build          # (etc. — see per-SDK AGENTS.md for direct tool commands)\n./run example android install   # build + install + launch the Kotlin minimal example\n./run codegen                   # regenerate every language binding from idl/\n```\n\nFor the direct `swift build` / `./gradlew` / `melos` / `yarn` / `npm` commands each SDK\nuses under the hood, and its test/lint/publish targets, see that SDK's own `AGENTS.md`\n(`bindings/<name>/AGENTS.md`) — the commands are kept there, not duplicated here, so they\ncan't drift.\n\nCross-platform build scripts worth knowing about at the root level:\n\n```bash\n./bindings/swift/scripts/build-core-xcframework.sh   # also syncs into RN/Flutter plugin dirs\n./scripts/build/build-core-android.sh                # .so for all ABIs → every SDK's jniLibs/\n./bindings/web/scripts/build-core-wasm.sh\n./scripts/setup/setup-toolchain.sh                   # protoc/wire/ts-proto toolchain\n./idl/codegen/generate_all.sh [--only <lang>]         # swift|kotlin|dart|ts|cpp|python\n```\n\n---\n\n## Version management and releases\n\nCanonical version: `core/VERSION` (single-line semver). Bump everywhere with:\n\n```bash\n./scripts/release/sync-versions.sh <version>\n```\n\nThe full release runbook (version bump → PR → tag → GitHub Release → npm/Maven/pub.dev/\nSwiftPM publishing → cutting the `runanywhere-swift` SPM distribution repo → starter-app\nsmoke tests) is a multi-step process captured in three skills — use them rather than\nimprovising the steps: **sdk-release** (version bump through published GitHub Release),\n**sdk-publish** (registry publishing + the `runanywhere-swift` dist repo cut), and\n**sdk-test-starters** (post-release device/app smoke tests).\n\n### Release pipeline lessons (v0.20.29 through v0.20.33 — read before cutting a release by hand)\n\n- **`native_ios` and `native_android` always rebuild fresh archives on every `release.yml`\n  run — there is no reuse/skip mechanism for either job** (unlike `native_web`'s\n  `reuse_native_web_run_id` or the cpp-desktop kits' `reuse_cpp_desktop_run_id`). This makes\n  the Apple/Android TEMP-pin convention (Package.swift's `sdkVersion`, the Flutter podspecs'\n  `asset_version`, Kotlin's `nativeLibVersion` pinned to an old version \"because this release\n  ships no new iOS/Android archives\") **structurally unsound for any release where those two\n  jobs actually run — which is every release.** It surfaced as three separate bugs\n  (`auto-tag.yml`'s coherence check, `package-sdk.sh`'s podspec check, and `sync-checksums.sh`'s\n  publish-time verification) before the pattern was recognized. If a release genuinely ships\n  no new Apple/Android bytes, that has to be proven by diffing the actual built artifacts\n  against the previous release, not assumed from what changed in the diff.\n- **`RunAnywhereMLXRuntime`'s stripped Mach-O is not byte-reproducible across builds**\n  (`RAC_CHECKSUM_SKIP=RunAnywhereMLXRuntime` in `release.yml`'s publish job exists because of\n  this, and is the ONLY archive exempted from the publish-time checksum gate). The corollary\n  that bit v0.20.29: **the checksum committed to `Package.swift` / the Flutter MLX podspec\n  can only be computed from a build that already happened, but the tag that freezes those\n  files has to be pushed before `native_ios` (which produces that exact artifact) even runs**\n  — so re-syncing checksums against an EARLIER, superseded build (e.g. after a tag had to be\n  moved and re-dispatched to fix an unrelated bug) commits a value that does not match what\n  the FINAL successful run actually publishes, and the skip means nothing catches it. v0.20.29\n  shipped this way (committed `d942788f...`, actually published `fc4b23a0...`) — caught only\n  because the Flutter `runanywhere_mlx` package hadn't been published to pub.dev yet for that\n  version. Before publishing any package that ships `RunAnywhereMLXRuntime`, re-run\n  `bindings/swift/scripts/sync-checksums.sh` against the artifacts from the SPECIFIC run whose\n  tag is being published — never reuse a checksum computed against an earlier attempt.\n- **The Electron QHexRT backend never set `ADSP_LIBRARY_PATH`, and one comment in the\n  codebase confidently asserted Windows doesn't need it.** It does, for exactly one decode\n  path: `QHexRT/src/bonsai/fastrpc_win.cpp`'s custom FastRPC skel (the ternary/Bonsai\n  decoder, e.g. `qwen3.8-27b-1bit-npu`) resolves through `ADSP_LIBRARY_PATH ∪ cwd`, separately\n  from the standard QNN HTP graph path's `LoadLibraryW` + PATH search that `bridge.ts`'s\n  `addSidecarDirToDllSearch` already handled. Every OTHER QHexRT model worked fine in the\n  packaged app, which is exactly what let this ship undetected — validate the Bonsai/ternary\n  path specifically, not just \"some NPU model works.\" Fixed in `bridge.ts`'s\n  `addSidecarDirToDspSearchPath` (#803).\n- **`package-private-engine-overlay.sh`'s QAIRT-runtime copy step filtered on `{\".dll\",\n  \".lib\"}` only**, silently dropping every `.so`/`.cat` skel file (both the standard HTP skel\n  and, since it never read `engines/qhexrt/prebuilt/current/dsp/win-arm64/` at all, the\n  Bonsai skel too). This meant **RCLI could not have run any QHexRT model** — standard or\n  ternary — from a fresh private overlay, a gap invisible until someone actually tried to run\n  inference rather than just building. `bindings/electron/scripts/bundle-native.ts` already\n  had the correct pattern (copy `dsp/win-arm64/` flat, no extension filter); the overlay\n  script just never matched it. Fixed in #804.\n- **A new NeuRT archive must be added in FOUR places, and three of them only fail in a build\n  configuration your Mac never produces.** v0.20.33 added `libneurt_rac_tts_ops.a` and took two\n  wasted release candidates to land, because each site fails somewhere different and late:\n  `engines/neurt/CMakeLists.txt` (probe list + imported target + `_neurt_extra_links`),\n  `build-core-xcframework.sh` (pre-flight guard AND `_NEURT_ROUTABLE_ARCHIVES` — two lists in one\n  file), `package-private-engine-overlay.sh`, and the `#if RAC_NEURT_ROUTABLE` guard around the\n  vtable entry in `rac_plugin_entry_neurt.cpp`. Miss the force-load lists and every native job goes\n  green while `validate_consumer_flutter` dies an hour later on `Undefined symbol:\n  _g_neurt_tts_ops` — the engine registers, all CMake targets compile, and only a CONSUMER's link\n  fails. Miss the vtable guard and the routable build is fine while every PR job WITHOUT\n  `NEURUN_TOKEN` fails, because a non-routable shell still references a symbol nothing defines.\n  A dev machine with the payload staged is routable, so it reproduces neither. `libneurt_rac_*.a`\n  archives now fail packaging by name if they are not in the list\n  (`neurt_assert_no_unlisted_archives`); the vtable guard has no such mechanism — audit all ten\n  slots when adding one. Reproduce a non-routable build by moving `core/third_party/neurt` aside.\n- **`reuse_native_web_run_id` is SAME-VERSION-ONLY, whatever the diff looks like.** `racommons.wasm`\n  embeds `RAC_VERSION_STRING` and `validate_public_packages.py` asserts it against the version being\n  packaged, so reusing ANY prior release's run fails `sdk_web` with *\"does not embed\n  RAC_VERSION_STRING\"*. Worse than a red job: `native_web` is skipped, so that candidate never\n  uploads a web artifact and can never serve as `publish_from_run_id`. Only a re-dispatch at the\n  same version qualifies.\n- **Verify checksums by NAME as well as by URL — the manifests use two different shapes.** The root\n  `Package.swift` interpolates `\\(sdkVersion)` into each URL; the Flutter manifests bind a checksum\n  to a `name:` through a helper with no URL at the call site at all. A URL-only cross-check silently\n  validates 9 of 15 sites and reports success. Re-hash every site against the zip it names, and\n  remember `RAC_CHECKSUM_SKIP` hides `RunAnywhereMLXRuntime` from `publish` entirely, so the Flutter\n  MLX podspec is verifiable only by hand.\n- **A green `rcli backends --json` proves nothing against the OSS kit.** NeuRT ships as a separate\n  private pack, so the public bottle lists no `neurt` at all — a routability check there passes\n  cleanly and means nothing. Apply the private overlay\n  (`cpp-desktop-macos-arm64-neurt-private` workflow artifact) before believing the result.\n- **General pattern across all four of the above**: each one was invisible to the checks that\n  existed (a version-coherence check, a checksum-skip escape hatch, a \"the model loaded\"\n  smoke test, a \"the build succeeded\" CI job) because none of those checks actually ran the\n  specific thing that was broken. When validating a release, run the SPECIFIC flagship/hardest\n  model end-to-end on-device, not just confirm the pipeline completed.\n- **The RACommons/RunAnywhereMLXRuntime checksum-staleness bug above recurred on v0.20.30 AND\n  v0.20.31** — same `mismatch: RACommonsBinary` failure in `publish`, same root cause both\n  times: the version-bump PR ran `sync-versions.sh` and merged/tagged directly, skipping the\n  **mandatory** \"dispatch a release candidate off the unmerged branch, sync checksums from\n  its real build, THEN merge\" sequence that the **`sdk-release` skill's step 2-3 already\n  document in detail** (including this exact failure signature). Documenting the lesson in\n  prose here was not enough to stop it recurring twice more — **if you are about to run\n  `scripts/release/sync-versions.sh` for ANY reason, including as one step of a larger\n  cross-repo release cascade, invoke the `sdk-release` skill and follow it from step 1**,\n  rather than reconstructing the sequence from memory or treating a \"small\" version bump as\n  exempt. v0.20.31 additionally caught `RunAnywhereMLXRuntime` in the Flutter podspec\n  independently stale in the SAME commit — `RAC_CHECKSUM_SKIP` exempts it from `publish`'s\n  check, so it would have shipped broken to Flutter consumers with no CI signal at all; check\n  every row of the checksum-location table, not only the one the CI error names.\n- **QHexRT's `qhx_runtime_device()` (arch/soc lookup, needed to pick the `v75`/`v79`/`v81`\n  manifest directory in `session_open()`) shared its live-device cost with the standard QNN\n  graph path**, so a `host_only` model with ZERO QNN graphs — the Bonsai/Maple ternary decoder\n  (`qwen3.8-27b-1bit-npu`) — paid for a live QNN HTP device anyway, which then contended with\n  the decoder's own direct FastRPC session for the same Hexagon cDSP and made every generation\n  fail with a bare `HostOpFailed`. This shipped in v0.20.30 (the SAME release that fixed the\n  `ADSP_LIBRARY_PATH` bug for the same model) and was found only by running that SPECIFIC model\n  on real Windows-ARM64 hardware after the \"fix\" — reinforcing the point above: a green\n  `HostOpFailed`-free run of a DIFFERENT QHexRT model proves nothing about this one. Two red\n  herrings were chased and ruled out with live device tracing before the real cause: neither\n  `ADSP_LIBRARY_PATH` (confirmed correctly set) nor a QAIRT-version mismatch between the\n  shipped host DLLs and the on-device skel (confirmed present, but fixing it alone did not fix\n  the failure) was the actual cause. Fixed in `neurun` v0.20.31 (`Backend::profile()` now uses\n  a separate, device-handle-free `deviceGetPlatformInfo` query instead of sharing\n  `ensure_device()` with `device()`) + this repo's `qhexrt_session.cpp` (#810, a complementary\n  fix removing a second, redundant eager device query used only for a log line).\n\n---\n\n## CI/CD workflows (`.github/workflows/`)\n\n| Workflow | Trigger | Purpose |\n|----------|---------|---------|\n| `pr-build.yml` | PR to main, push to main/feat branch | Parallel native builds (macOS/Linux/iOS/Android) + per-SDK typecheck + centralization/coherence gates |\n| `release.yml` | Tag `v*.*.*` or manual | Full artifact build matrix, SDK packaging, consumer validation, draft Release |\n| `auto-tag.yml` | PR merged to main with `release:*` label | Verifies the reviewed semver bump, pushes that exact git tag |\n| `idl-drift-check.yml` | Changes to `idl/` or generated files | Regenerates protos, verifies schema lock + untracked generated trees |\n| `legacy-files-blocklist.yml` | All PRs/pushes | Prevents specific deleted files from being re-introduced |\n| `secret-scan.yml` | PRs and pushes to main | Incremental gitleaks scan on diff range |\n| `check-no-pii-logging.yml` | PRs/pushes to main/master/feat-branch | Guards against logging signed URLs alongside active-download destination paths |\n\n---\n\n## Platform requirements\n\n| Platform | Min version | Build tool | Key versions |\n|----------|------------|------------|--------------|\n| iOS / macOS | 17.5 / 14.5 | Xcode 26+ | Swift 6.2 |\n| Kotlin SDK | Android API 24 | AGP 9.2.1 / Gradle 9.5.0 | Kotlin 2.4.0, NDK 27.3.13750724 |\n| Flutter | 3.44.6 | Melos / AGP 9.0.1 / Gradle 9.1.0 | Dart 3.12.2+, NDK 28.2.13676358 |\n| React Native | 0.85.3 (min 0.83.1) | Yarn Berry 3.6.1 | NitroModules, Hermes |\n| Web | Chrome 86+ | Vite | Emscripten 6.0.2, Node 24 LTS |\n| C++ core | N/A | CMake 3.24+ | C++20, Ninja |\n\n---\n\n## Non-obvious configuration details\n\n- **NDK pin**: `core/VERSIONS::NDK_VERSION` (27.3.13750724) is the single source of truth,\n  mirrored into `bindings/kotlin/gradle.properties`. NDK 27 (r27d) gives 16 KB\n  page-alignment required by Android 15+ — NDK 25.x's 4 KB-aligned `libc++_shared.so` /\n  `libomp.so` trips Android 16's page-size enforcement. Mirror this pin whenever bumping\n  it; Flutter/RN Android build files carry their own fallback literals that can drift.\n- **`useLocalNatives`** (Kotlin `gradle.properties`, similarly named flags elsewhere):\n  `true` builds native libs from source locally; CI and most non-local runs set it\n  `false` to download prebuilt `.so`/`.xcframework`/`.wasm` from GitHub Releases instead.\n- **Web SDK** has several hard-won WASM/browser runtime workarounds (VLM worker crash\n  recovery, a Qwen2-VL WebGPU NaN bug, cross-origin isolation requirements) — see\n  [`docs/reference/web-runtime-gotchas.md`](docs/reference/web-runtime-gotchas.md).\n- **Cloud/Linux dev VM** (e.g. Cursor Cloud): what's buildable there and the\n  clang/NDK/KVM gotchas are in\n  [`docs/reference/cursor-cloud-environment.md`](docs/reference/cursor-cloud-environment.md).\n\n---\n\n## Pre-commit hooks\n\n```bash\npre-commit run --all-files                     # everything\npre-commit run ios-sdk-swiftlint --all-files   # SwiftLint only\n```\n\nConfigured hooks: gitleaks (secrets), trailing-whitespace, end-of-file-fixer, check-yaml,\ncheck-added-large-files (1000 KB max), check-merge-conflict, object file detection,\nSwiftLint (SDK + example app), periphery (unused Swift code detection).\n\n---\n\n## Where to look for current in-flight work\n\n`thoughts/shared/plans/` holds active/completed execution plans (including any in-flight\nSDK reorg or migration briefs — e.g. the Electron reorganization plan); `thoughts/shared/issues/`\nholds tracked bug write-ups. Check these directories directly rather than trusting a\nstatus summary written into this file — both change too often for a static snapshot here\nto stay accurate.\n",".claude/skills/sdk-test-starters/SKILL.md":"---\nname: sdk-test-starters\ndescription: Run a REAL end-to-end inference smoke test (model download + load + generate, with visible output) on every RunAnywhere starter app — iOS, macOS/Swift, Android, Web, Electron — after a runanywhere-sdks release. Build success alone is never sufficient evidence.\n---\n# SDK Test Starters\n\nPost-release validation runbook for the five RunAnywhere SDK consumer apps. This\nskill exists because \"it built\" has been reported as \"tested\" before, and that is\nnot evidence of anything except that the compiler ran.\n\n## 0. The evidence bar (applies to every app, no exceptions)\n\nFor each app you must produce, and be able to state explicitly:\n\n1. **Version proof** — print/log the actual resolved SDK version from inside the\n   running app (a startup log line, a version screen, an `swift package show-dependencies`\n   / `./gradlew dependencies` / `npm ls @runanywhere/...` output, etc). Never assume\n   a version bump \"took\" just because you edited a manifest.\n2. **Real model acquired** — a real (small) model downloaded fresh, or confirmed\n   already cached, by the app itself (not a file you hand-placed unless that IS the\n   app's supported flow).\n3. **Load** — the model actually loads (watch for a load-succeeded log/state, not\n   just \"no crash\").\n4. **One real inference call** — LLM `generate()` is almost always the easiest\n   modality to drive; use it unless another modality is genuinely less work for\n   that specific app.\n5. **Visible output** — capture the actual generated text (or audio/image) and\n   quote it in your report. \"It responded\" is not enough — show what it said.\n6. **Logs reviewed** — read the console/log stream for errors and warnings and\n   explicitly explain any that appear. Do not silently drop a stack trace because\n   the app \"still produced output.\"\n\nReporting an app as tested from a build/compile success alone is a skill violation.\nIf you could not get past build, that is a BLOCKED report (see §8), not a PASS.\n\n## 1. Locating each app's real local checkout\n\nThis machine accumulates many differently-named copies of the same repos over time\n(worktrees, experiment clones, old branches, scratch dirs). **Never trust a\ndirectory name.** Before touching anything in a candidate directory:\n\n```bash\ngit remote get-url origin          # must be https://github.com/RunanywhereAI/<repo>.git\ngit status --short --branch        # what branch, what's dirty, is it someone else's WIP?\n```\n\nAs of this writing, the known-good local checkouts for this user are here (verify\nthis each time — do not hardcode it further than \"check this location first\"):\n\n```\n~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-ios       -> RunanywhereAI/runanywhere-ios.git\n~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-android   -> RunanywhereAI/runanywhere-android.git\n~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-web       -> RunanywhereAI/runanywhere-web.git\n~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-electron  -> RunanywhereAI/runanywhere-electron.git\n```\n\nThe macOS/Swift case has no separate consumer repo — see §4, it lives inside\n`~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks` (confirm with\n`git remote get-url origin` there too — this machine also has stray full-monorepo\nclones elsewhere, e.g. under `~/development/hard/ambient/`, that are NOT the one to\nuse unless you've verified it's the intended checkout).\n\nThese starter checkouts are commonly sitting on a feature branch, not `main` — e.g.\nat last check `runanywhere-ios` and `runanywhere-android` were on\n`add-gemma4-qwen3.6-qwen3.8-models` and `runanywhere-web` on `add-supertonic-3-tts`.\nThat is fine to test FROM, but say so in your report, and do not `git checkout` away\nfrom a branch that has uncommitted work you didn't create — if `git status --short`\nshows dirty files you don't recognize, stop and ask rather than stashing/resetting.\n\nIf genuinely unsure which directory is real, `git clone` a fresh one into the\nscratchpad rather than guessing — a wrong guess wastes far more time than a clone.\n\n## 2. Cross-repo facts this skill assumes\n\n- SDK version SoT: `core/VERSION` in `runanywhere-sdks`, bumped everywhere by\n  `scripts/release/sync-versions.sh <version>`.\n- Release publish is `.github/workflows/release.yml` (dispatch or `v*.*.*` tag).\n- **Never** treat a build success against `RUNANYWHERE_USE_LOCAL_NATIVES=1` (or any\n  monorepo-local staged artifact) as proof the *public* release works — see §4.\n- QHexRT/QNN material must never appear in a public artifact — release.yml\n  hard-fails on it. If anything you're testing surfaces a QHexRT asset in a public\n  package, stop and flag it, don't route around it.\n\n## 3. iOS — `runanywhere-ios`\n\nSDK pin lives in `Package.swift`:\n\n```swift\n.package(url: \"https://github.com/RunanywhereAI/runanywhere-swift.git\", from: \"0.20.19\")\n```\n\nThis only resolves once `runanywhere-swift` (the generated Swift-only SPM\ndistribution repo — see `~/.claude/skills/sdk-publish/SKILL.md` §4) is\nactually tagged at the target version. Steps:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-ios\n# bump the `from:` version in Package.swift to the target release, then:\nswift package update            # or open in Xcode and let it resolve\n./scripts/build_and_run_ios_sample.sh simulator \"iPhone 16 Pro\"\n```\n\n`./scripts/verify.sh` is the CI-equivalent gate: it resolves the package remotely\nand runs a full `xcodebuild` against the iOS Simulator — use it to prove the\nresolve is clean (it fails if `Package.resolved` ends up dirty, i.e. the committed\npin was stale). `./scripts/smoke.sh` is a fast, non-compiling grep-based check —\nit does NOT count as evidence for this skill's bar, only `build_and_run` +\nwatching real output does.\n\nDrive one real `generate()` call through the app UI (or the equivalent debug\naction if the sample exposes one) against a small downloaded model, then watch the\nruntime log stream for the version line, the model download/load, the generation,\nand any errors:\n\n```bash\nlog stream --predicate 'subsystem CONTAINS \"com.runanywhere\"' --info --debug\n```\n\nMost loggers use `com.runanywhere.RunAnywhereAI`; a few use plain\n`com.runanywhere`; the SDK logs under its own subsystem — the `CONTAINS` match\ncovers all of them. On a physical device use `idevicesyslog | grep \"com.runanywhere\"`\ninstead.\n\n## 4. macOS/Swift — no separate repo, use the in-tree example or rcli\n\nThere is no `runanywhere-macos` consumer repo. Two in-monorepo options, both inside\n`~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks`:\n\n- `bindings/swift/example/` — a minimal SwiftPM example app/executable.\n- `rcli/` — the C++ core's CLI, `rac_*` C ABI consumer.\n\n**The critical gotcha**: the repo's own `Package.swift` is fail-closed toward the\n*real published release* — it resolves the remote `binaryTargets` unless you\nexplicitly opt into locally staged XCFrameworks with\n`RUNANYWHERE_USE_LOCAL_NATIVES=1`. Every doc/script example in this repo sets that\nenv var (because local dev wants fast iteration against in-progress binaries) —\nbut that means copy-pasting those examples verbatim tests your LOCAL staged build,\nnot the actual public release artifact. To prove the published release works:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks/bindings/swift/example\n# DO NOT set RUNANYWHERE_USE_LOCAL_NATIVES — leave it unset so Package.swift\n# resolves the real remote release binaries for the target version.\nswift build\nswift run runanywhere-minimal \"Name three colours.\"\n```\n\nIf the example was previously built/run with the env var set, `swift build` may\nreuse a stale resolve — run `swift package reset` (or delete `.build/`) first if\nyou're not sure which mode the last resolve used.\n\nFor rcli instead (also validates the real published core):\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks\nrcli models download qwen3          # ~639MB, small enough for a quick smoke test\nrcli llm generate --model qwen3 \"Reply with exactly: RCLI WORKS\" --reasoning off\n```\n\n`rcli models list --all` shows the rest of the built-in catalog\n(`whisper-tiny`, `smolvlm2`, `piper`, …) if LLM generate isn't the right modality\nfor what you're verifying.\n\n## 5. Android — `runanywhere-android`\n\nVersion pin: `gradle/libs.versions.toml` has ONE shared catalog entry:\n\n```toml\nrunanywhere = \"0.20.19\"\n...\nrunanywhere-sdk      = { group = \"io.github.sanchitmonga22\", name = \"runanywhere-sdk\",              version.ref = \"runanywhere\" }\nrunanywhere-llamacpp = { group = \"io.github.sanchitmonga22\", name = \"runanywhere-llamacpp\",          version.ref = \"runanywhere\" }\nrunanywhere-onnx     = { group = \"io.github.sanchitmonga22\", name = \"runanywhere-onnx\",               version.ref = \"runanywhere\" }\nrunanywhere-qhexrt   = { group = \"io.github.sanchitmonga22\", name = \"runanywhere-qhexrt-android\",     version.ref = \"runanywhere\" }\n```\n\n**CRITICAL — do not bump this block as one unit.** `runanywhere-qhexrt-android` is\npermanently frozen at whatever version it was last published at before being\nexcluded from the public Maven train; by policy it can never move forward via\npublic Maven again. Verify the actual frozen version FRESH every time (do not\ntrust a remembered number — it doesn't change, but don't skip the check):\n\n```bash\ncurl -s https://repo1.maven.org/maven2/io/github/sanchitmonga22/runanywhere-qhexrt-android/maven-metadata.xml\n```\n\nThe other three artifacts (`runanywhere-sdk`, `-llamacpp`, `-onnx`) CAN and should\nmove to the new release version. This means the single `runanywhere` catalog ref\nmust be **split into two entries** — one at the new version for sdk/llamacpp/onnx,\none pinned to the frozen qhexrt version — not bumped as a block.\n\n**Before trusting a mixed pin works**: verify `RAC_PLUGIN_API_VERSION` (the\nplugin ABI version constant in the C++ core, exposed identically to Kotlin) is\nunchanged between the old public SDK version and the new one. If it changed, mixing\na newer sdk/llamacpp/onnx with the frozen qhexrt version is NOT safe — this is a\nhuman decision (needs either an ABI-compat shim or dropping qhexrt from this build),\nnot something to force through.\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-android\n./gradlew :app:assembleDebug\n./gradlew :app:installDebug        # installs to a connected emulator/device\nadb logcat | grep -i runanywhere   # or a more specific tag if the app uses one\n```\n\nLaunch the app, drive one real `generate()` call against a small downloaded model\n(the app's own model picker/download flow), confirm the resolved SDK version is\nlogged, and review logcat for errors before reporting a PASS.\n\n## 6. Web — `runanywhere-web`\n\n`package.json` has 4 `@runanywhere/*` deps pinned with a caret range:\n\n```json\n\"@runanywhere/proto-ts\":    \"^0.20.19\",\n\"@runanywhere/web\":         \"^0.20.19\",\n\"@runanywhere/web-llamacpp\":\"^0.20.19\",\n\"@runanywhere/web-onnx\":    \"^0.20.19\"\n```\n\nBump all 4 to the new version, then:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-web\nnpm install\nnpm run build\nnpm run dev      # vite --host localhost --port 3000 --strictPort\n```\n\nThe dev/preview server is what supplies COOP/COEP headers required for\n`SharedArrayBuffer` — WASM inference will silently fail (or refuse to init\nthreads) without them, so don't swap in a bare static file server.\n\n`~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks/bindings/web/AGENTS.md`\nhas the authoritative Validation section (its own heading is `## Validation`) —\nfull checklist:\n\n1. Fresh browser context.\n2. Example app served with COOP/COEP headers.\n3. Model download.\n4. Model load.\n5. Real browser inference for the target modality.\n6. Logs/screenshots reviewed.\n\nIts \"Required release gates\" (run before browser validation) from the same repo:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks/bindings/web\nnpm run typecheck && npm run lint && npm run test && npm run build\nnpm run test:browser\nnpm run test:browser:release\ncd example && npm run typecheck && npm run build\n```\n\nStatic gates passing is not the smoke test — you still need an actual browser\nwatching real inference happen against the starter app. Prefer a real browser\nautomation tool (`claude-in-chrome` MCP tools, or Playwright — the maintained\nsuites under `bindings/web/tests/browser/` already use `playwright.config.ts`)\nover curling the served page: WASM inference genuinely needs a browser JS engine.\nLoad the served starter app, download/load a small model, trigger generate, and\nread the actual rendered output text plus the browser console — do not infer\nsuccess from an HTTP 200 on the page load.\n\n## 7. Electron — `runanywhere-electron` (SPECIAL CASE)\n\n**Do not assume a fresh SDK publish is available for this app.** Electron's own\nnpm packages (`@runanywhere/electron`, `-llamacpp`, `-onnx`, `-sherpa`, `-qhexrt`,\n`-neurt`) are on a SEPARATE, manual-publish pipeline — NOT part of `release.yml`\n(grep confirms zero mentions of these package names there). This separation is a\n**confirmed, deliberate** repo-owner decision, not a gap. Check\n`~/.claude/skills/sdk-publish/SKILL.md` §6 before assuming anything moved.\nElectron is **no longer WIP on a branch** — the old \"in-progress on\n`smonga/electron_upgrade`\" note is gone from the root `AGENTS.md`/`CLAUDE.md`\nentirely as of this checkout; `bindings/electron/` is a fully merged SDK on `main`.\nAs of `fix/electron-cicd-staging-url-npu-ane`,\n`.github/workflows/electron-native-package.yml` builds every backend for real and\npackages every tarball — but still does not publish; verify against a live run\nrather than assuming build automation status either way.\n\nTest the starter against whatever version it is CURRENTLY pinned to unless a human\nhas explicitly confirmed a newer Electron publish is ready and intended:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-electron\ncat package.json | grep '@runanywhere/electron'   # confirm current pin before touching it\n```\n\n**`@runanywhere/electron-qhexrt` being public on npm is a CONFIRMED, settled\npolicy decision** (repo owner, 2026-08-20), not a contradiction to flag — it\ncontains real Hexagon NPU binaries (`libQnnHtpV81Skel.so`, `QnnHtp*.dll`,\n`rac_commons.dll`, `runanywhere_qhexrt.dll`) on purpose. The QHexRT-stays-private\npolicy enforced elsewhere in this repo family is specifically about the\nGitHub-Release-side asset set (Android etc.), a different channel. Only re-raise\nthis if you find evidence the policy changed.\n\nTwo layers to actually exercise, do not conflate them:\n\n**(a) Cross-platform Electron JS + macOS native addon** — buildable and testable\nlocally on this Mac right now:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-electron\nnpm install\nnpm run build   # or the app's documented dev/start script — check package.json scripts\nnpm start        # or equivalent — launch the actual Electron app\n```\n\nDrive one real `generate()` call in the running app, watch its console/log output\nfor the resolved SDK version, model download, load, and generation, and read any\nerrors rather than ignoring them.\n\n**(b) Genuine Windows-native behavior** — a macOS build proves NOTHING here\nregardless of how clean the cross-platform code looks; the native addon and any\nWindows-only DLLs must be exercised on real Windows.\n\nThe team keeps a **Windows ARM64 test machine reachable over SSH**. Its hostname,\nuser, key, and VPN/mesh node name are operator-specific and deliberately not\nrecorded in this repo — read them out of the operator's own `~/.ssh/config` (look\nfor the Windows/ARM64 host entry) and use that alias below wherever `$WIN_BOX`\nappears. If no such entry exists, the box is not configured for you: say so and\nask, rather than guessing at a hostname.\n\n```bash\nWIN_BOX=<the ssh alias from ~/.ssh/config>\n```\n\n**Always check reachability first, never just try SSH and retry on timeout.** The\nbox lives on a private mesh VPN and is frequently asleep, so start by asking the\nVPN client whether the node is up (e.g. `tailscale status`, matching on the host\nentry's `HostName`) rather than opening a connection.\n\nIf the node reports `offline, last seen ...`, the box is asleep/powered off.\n**A connection timeout here means \"device asleep,\" not \"config is wrong.\"** Tell\nthe human and ask them to wake it; do not loop retrying SSH. Only once the node\nshows a live/idle state, confirm with:\n\n```bash\nssh -o ConnectTimeout=8 \"$WIN_BOX\" \"whoami\"\n```\n\nThen, with the repo already cloned there:\n\n```bash\nssh \"$WIN_BOX\" \"cd <repo-path-on-that-box>; git pull; npm install; npm run build\"\n# then run whatever launches the Windows Electron build and drive a real generate call\nscp \"$WIN_BOX\":<remote-log-or-artifact-path> ./local-copy   # pull back evidence to inspect\n```\n\nDo not claim Windows-specific validation happened unless you actually obtained a\nshell on that box and ran something real there — a clean macOS build is not\nsubstitute evidence.\n\n**`node_modules/electron` on this box silently tracks whichever arch last ran `npm\ninstall`, not which Node launches the test.** The x64 (llamacpp/onnx/sherpa) and\narm64 (QHexRT) lanes share ONE `runanywhere-electron` checkout/`node_modules` tree.\nPrepending an x64 Node to `PATH` and running `npx playwright test` does NOT make\nElectron itself x64 — `require('electron')` always resolves the single installed\n`node_modules/electron/dist/electron.exe`, and whichever arch that binary actually\nis determines which `prebuilds/win32-<arch>/` directory the running app looks in,\nregardless of `scripts/use-sdk.mjs local`'s staging (which itself is arch-aware\nbased on the *launching* Node, so it can correctly report \"staged win32-x64\" while\nthe process that will actually run is arm64 — a silent mismatch). Verify with:\n`powershell -Command \"$p=[IO.File]::ReadAllBytes('...\\electron.exe'); $peOff=\n[BitConverter]::ToInt32($p,60); [BitConverter]::ToUInt16($p,$peOff+4)\"` — `43620`\n(`0xAA64`) is ARM64, `34404` (`0x8664`) is x64. If it's the wrong arch (e.g. a prior\nsession's `npm install` for the other lane silently overwrote it), a genuine re-test\nof the other lane needs a FULLY SEPARATE checkout/`node_modules` tree installed\nunder the matching-arch Node — don't assume PATH-prepending alone is sufficient, and\ndon't trust `use-sdk.mjs status`'s \"platform: win32-x64\" output as proof the running\nElectron process is actually x64.\n\n**NPU hardware is a scarce, single-owner resource — serialize, never parallelize.**\nThis box has ONE physical Hexagon NPU. `engines/qhexrt/qhexrt_session.cpp`'s\n`session_open()` has a load-bearing comment on this exact failure mode: the HTP\nexposes a small number of protection domains with a per-PD context ceiling, so a\n*second* concurrent model load fails with `RAC_ERROR_BACKEND_INIT_FAILED`\n(\"Backend initialization failed\") — a fast (tens-of-ms), generic-looking failure\nthat is easy to mistake for a real regression. Confirmed once tonight: two agents\neach independently exercising QHexRT on this box at the same time produced exactly\nthis failure. Before concluding a QHexRT test genuinely failed, confirm nothing\nelse (another agent, a leftover Electron process, a stuck previous test run) is\nalso holding an NPU session open on this box — check for lingering `electron.exe`/\n`node.exe` processes from a prior run and kill them if orphaned, and never launch\ntwo NPU-touching verification passes concurrently on this machine.\n\n**\"Backend initialization failed\" has (at least) three distinct root causes —\ndon't stop at PD exhaustion.** Confirmed on 2026-08-17 after PD-exhaustion and\nstaleness were both ruled out (fresh rebuild, source-identical prebuilt, no other\nprocess holding a session): the *actual* bug was two compounding issues, both now\nfixed/documented —\n1. **Real source bug, fixed in PR #733**: `qhexrt_session.cpp`'s `runtime_acquire()`\n   called `qhx_runtime_create(nullptr, nullptr)` on every non-Android platform,\n   trusting the OS default DLL search order to find `QnnHtp.dll`/`QnnSystem.dll`.\n   On Windows that order never includes a dynamically-loaded sibling plugin's own\n   directory (`LOAD_WITH_ALTERED_SEARCH_PATH`, used to load `runanywhere_qhexrt.dll`\n   itself, only widens the search for *that* load call, not later `LoadLibrary`\n   calls made by code already running inside it) — so the QNN DLLs were invisible\n   even sitting right next to the plugin. Fixed by resolving the plugin's own\n   module directory (`GetModuleHandleExA`+`GetModuleFileNameA`) and passing\n   explicit paths, mirroring Android's existing `ADSP_LIBRARY_PATH` resolution in\n   the same file.\n2. **Operational staleness, NOT a script bug**: even after (1), the QNN runtime\n   DLLs actually staged into `@runanywhere/electron-qhexrt`'s\n   `prebuilds/win32-arm64` (`node_modules/.../electron-qhexrt/prebuilds/win32-arm64/`)\n   were from an old QAIRT build (~2.45.x vintage, Dec 2025 files) while the box's\n   installed QAIRT was 2.48.0.260626 — a real ABI-relevant version mismatch.\n   `bindings/electron/scripts/bundle-native.ts`'s `stageQnnRuntime()` just mirrors\n   whatever `RA_QNN_RUNTIME_DIR` points at with **no version check** — it's\n   correct-as-written; someone had pointed it at a stale neurun-side\n   `qairt-runtime-248`-named folder that was never refreshed. Fix: rebuild that\n   flat dir from the CURRENT QAIRT SDK's `lib/aarch64-windows-msvc/` (host DLLs:\n   `QnnHtp.dll`, `QnnHtpPrepare.dll`, `QnnHtpV81Stub.dll`,\n   `QnnHtpV81CalculatorStub.dll`, `QnnSystem.dll`) + `lib/hexagon-v81/unsigned/`\n   (DSP side: `libQnnHtpV81Skel.so`, `libqnnhtpv81.cat`), point\n   `RA_QNN_RUNTIME_DIR` at it, and re-run\n   `npm run bundle:native -- --package=qhexrt` from `bindings/electron/`.\n   **The already-published `@runanywhere/electron-qhexrt` npm package (as of\n   0.20.24) almost certainly ships the same stale DLLs** — this needs a\n   human decision (republish/patch) before claiming the published package works\n   on Windows NPU, separate from the source fix in PR #733.\n3. **PD exhaustion** (the pre-existing entry above) remains real and still applies\n   when two sessions genuinely contend for the one physical NPU — but it is not\n   the only explanation, and the log line to actually check first is the native\n   one: subscribe via `window.runanywhere.logging.records()` (after\n   `logging.setDebugMode(true)`) or add a temporary main-process log capture —\n   `app.process().stdout`/`stderr` do NOT carry it, because the addon runs in a\n   separate `utilityProcess.fork()` host, not the Electron main process, and\n   `RAC_LOG` is not an env var the logger reads (it does nothing). The generic\n   `[QHexRT] qhx_runtime_create failed (QNN libs unavailable?)` vs.\n   `qhx_model_load failed: ... N NPU model(s) already resident` log text is what\n   actually distinguishes cause (1)/(2) from cause (3) — always get that line\n   before diagnosing further.\n\n**Workflow gotcha:** if you orchestrate this via the Workflow tool, its agent()\ncalls do NOT reliably block-wait on a long-running remote build the way a direct\npoll loop does — observed once tonight where a workflow's own agents spawned an\ninternal Monitor/poller for the build log and then returned immediately,\nreporting \"still waiting,\" which let the whole workflow finish with nothing\nactually verified. For \"wait N tens-of-minutes for a real native build to\nfinish,\" use the top-level Monitor tool yourself (a shell poll loop over SSH\nchecking for a terminal BUILD_SUCCEEDED/BUILD_FAILED marker line) rather than\ndelegating the wait itself into a workflow agent's own judgment.\n\n### The Windows box's actual layout, and why it's more than a cross-platform check\n\n**This is a Hexagon-NPU-equipped Windows machine** (Snapdragon-class ARM Windows\ndevice, Copilot+ PC), not just a generic x86 Windows box. That makes it the one\nplace a QHexRT/Hexagon backend build+test can produce real device-only truth for\nWindows — matching the parent workspace's own \"x86 is a simulator, device-only\ntruth\" rule (see the top-level `Qualcomm/CLAUDE.md` and `QHexRT/CLAUDE.md`). Don't\ntreat this box as merely a Windows Electron smoke-test target; it's the only real\nHexagon NPU test rig reachable for this platform.\n\nPer the user (2026-08-17), under `~/Downloads/RunAnywhere/` on that box there are\nthree sibling repo checkouts at the same level — **verify each on connect, don't\nassume paths without checking, since this is relayed secondhand and may have\ndrifted**:\n- `runanywhere-sdks` — this monorepo, presumably already cloned; `git fetch && git\n  log -1 origin/main` to see how far behind it is before doing anything.\n- `runanywhere-electron` — the Electron starter app (same repo as\n  `RunanywhereAI/runanywhere-electron` elsewhere in this skill).\n- a third repo referred to verbally as \"the neuron/new-run inference engine for the\n  Hexagon NPU\" — almost certainly **`neurun`**, the QHexRT/NeuRT split-out repo\n  (see memory: \"neurun component split merged — QHexRT/ + NeuRT/ + shared/, root is\n  a closed 20-entry set enforced by a test\"). Confirm the actual directory name and\n  remote (`git remote get-url origin`) on connect rather than guessing from the\n  transcription.\n\n**Full task on this box (per explicit user instruction, not just a smoke test):**\n1. `git pull` (or fetch+fast-forward) all three repos to latest `main`.\n2. Rebuild the QHexRT/Hexagon NPU backend from `neurun` against this device's real\n   Hexagon NPU — this is the one environment where that build+run is meaningful\n   proof, not a simulator claim.\n3. Bump Electron's native dependencies to their latest versions — llama.cpp,\n   sherpa-onnx, ONNX Runtime, and whatever else `bindings/electron` pins — across\n   the repos involved. Treat this as a real dependency-bump task (check for\n   breaking API changes, don't just edit a version string and assume it builds).\n4. Update the model catalog for **all 5 starter apps across all 4 consumer repos**\n   (iOS, Android, Web, Electron — plus whatever the \"5th app\" split means for\n   macOS/Swift, see the rest of this skill) so the catalog is consistent everywhere,\n   not just on this Windows box.\n5. Run a real end-to-end inference pass on this box specifically (not just build\n   success) — both the general Electron/llama.cpp path AND the Hexagon NPU path via\n   `neurun`/QHexRT.\n6. Open PRs for whatever changed, in each affected repo. Do not merge them —\n   the user explicitly said they'll review.\n\nThis is real engineering work (native dependency bumps + an NPU backend rebuild),\nnot a mechanical version-string edit — expect it to take real time and to\ngenuinely need `QHexRT/CLAUDE.md` and `neurun`'s own docs read in full before\ntouching that backend, per the parent workspace's own instructions.\n\n## 8. Reporting\n\nFor each of the 5 apps (iOS, macOS/Swift, Android, Web, Electron), state exactly\none of these two outcomes — never blur them:\n\n- **FULLY VERIFIED** — with concrete evidence: the version string actually\n  resolved/logged, the model used, and the actual output produced (quote it).\n- **BLOCKED** — with the exact concrete reason, e.g.: the Windows ARM64 box's\n  mesh-VPN node is offline; Android emulator unavailable in this environment; App Store /\n  notarization credential missing; `runanywhere-swift` not yet tagged at target\n  version so `swift package update` can't resolve; electron-qhexrt policy conflict\n  needs a human call before proceeding further.\n\nA clearly reported partial/blocked result is far more valuable than an optimistic\nsummary that overstates what was actually observed. If you hit the known Electron\nqhexrt npm-publish policy conflict (§7), call it out in the report even if it\nwasn't the thing blocking your specific test — it's a standing issue every release\nnight should re-surface until a human resolves it.\n\n## Credentials referenced but never printed by this skill\n\nThese exist and are checked for presence only — never print their values:\n\n- Maven Central: `mavenCentral.username` / `mavenCentral.password`,\n  `signing.gnupg.keyName` / `signing.gnupg.passphrase` in `~/.gradle/gradle.properties`.\n- GPG signing key `CC377A9928C7BB18` — check with\n  `gpg --list-secret-keys --keyid-format LONG`.\n- macOS notarization profile `runanywhere-notary` — check with\n  `xcrun notarytool history --keychain-profile runanywhere-notary`; codesign\n  identity `Developer ID Application: RunAnywhere, Inc` — check with\n  `security find-identity -v -p codesigning`.\n- npm — `npm whoami` should already show a logged-in user.\n\nNone of the app-level smoke tests in this skill require touching these directly;\nthey matter only if a test path requires a fresh distro-repo publish (e.g.\n`runanywhere-swift` not yet tagged) — that's a separate concern of the\n`sdk-publish` skill, not this one.\n","CLAUDE.md":"# AGENTS.md\n\nGuidance for AI coding assistants working in this repository.\n\n> **`AGENTS.md` is the real file; each `CLAUDE.md` is a committed symlink to the\n> `AGENTS.md` beside it.** Editing either name edits the same bytes. If you create a new\n> `AGENTS.md` in a directory that doesn't have one, run\n> `bash scripts/validation/gates/check_agents_claude_sync.sh --fix` from the repo root to\n> create its `CLAUDE.md` symlink — `pr-build.yml` fails any tracked `AGENTS.md` missing one.\n> Never hand-create a `CLAUDE.md` as a real file.\n\n## Working conventions\n\n- **Focus on simplicity.** Clean, SOLID-ish separation of concerns; reuse over\n  reinvention. No mock implementations and no new unit tests unless the task explicitly\n  asks for them.\n- **Plan first for non-trivial work.** Write the plan to\n  `thoughts/shared/plans/{descriptive_name}.md`, get it approved before implementing, and\n  keep it updated (append what changed) as you go so the next engineer can pick it up.\n- **Structured types over raw strings.** Enums/sealed classes/data classes for\n  configuration and results — this is enforced per-SDK by lint (ktlint/detekt, SwiftLint,\n  ESLint) so ad hoc strings tend to get caught anyway.\n- **Read files fully** before editing them; only use offset/limit on files too large to\n  read at once.\n- **Use full local build parallelism** (`-j \"$(sysctl -n hw.logicalcpu)\"` / equivalent),\n  scaling down only under real memory/thermal pressure — not because load average looks\n  high.\n- **Multi-agent workflows run on Sonnet.** When fanning work out to subagents (the\n  `Workflow` tool, or several parallel `Agent` calls), pass `model: 'sonnet'` on every\n  `agent(...)` call rather than letting them inherit the session model. A fan-out of\n  8-10 probes reading large files is exactly the shape where Sonnet is the right\n  cost/latency trade, and these agents are doing bounded, well-specified retrieval and\n  analysis rather than open-ended reasoning. Reserve the session's stronger model for\n  the synthesis step that consumes their reports, and for anything where a wrong answer\n  is expensive and hard to detect — a final design decision, an adversarial verification\n  pass, or a security-relevant judgement.\n\n  ```js\n  // in a Workflow script\n  const findings = await parallel(LENSES.map((l) => () =>\n    agent(l.prompt, { label: `probe:${l.key}`, schema: FINDINGS, model: 'sonnet' })))\n\n  // the synthesis that reads them can stay on the session model\n  const plan = await agent(synthesisPrompt, { schema: PLAN, effort: 'high' })\n  ```\n\n## The most important architectural rule: logic lives at the lowest layer that serves everyone\n\nEach feature/modality (LLM, STT, TTS, VAD, VLM, RAG, LoRA, Voice) is invoked through\n**one** SDK entry point; the SDK, and below it C++ commons, does all the heavy lifting. If\nan example app builds a multi-step bootstrap sequence, hardcodes a model/engine constant,\nor post-processes model output, that is a bug in the SDK, not the app — fix it down a\nlayer.\n\n1. **C++ commons (`core/`)** — cross-platform, non-I/O logic: model lifecycle, registry,\n   download orchestration, RAG session management, inference routing. All 5 SDKs get the\n   fix for free.\n2. **Platform SDK layer** — platform-specific I/O or runtime bridging only (Web OPFS,\n   iOS Keychain, Android Keystore, WASM MEMFS mirroring).\n3. **Example apps** — UI rendering, navigation, thin SDK calls. No business logic, no\n   internal-SDK knowledge (path patterns, framework→directory mappings), no workarounds.\n\n**iOS is the source of truth.** When behavior is ambiguous in any other SDK, check the\niOS Swift implementation first and copy the logic exactly, adapting only syntax.\n\n---\n\n## Repository overview\n\nCross-platform on-device AI SDK monorepo. A single C/C++ core (`runanywhere-commons`,\n~118K first-party LOC plus ~420K generated proto bindings) implements all AI business\nlogic behind a pure C ABI (`rac_*` prefix). Platform SDKs are thin bridges that supply\nplatform services (file I/O, HTTP, Keychain, audio) via an inversion-of-control struct and\ncall into the C core for all inference. Protobuf IDL schemas generate type-safe bindings\nfor every language.\n\n**Current version**: `0.20.37` (canonical source: `core/VERSION`)\n\n| SDK | Path | Bridge mechanism | Platforms |\n|-----|------|-------------------|-----------|\n| Swift | `bindings/swift/` | XCFramework + CRACommons module map | iOS 17.5+, macOS 14.5+ |\n| Kotlin | `bindings/kotlin/` | JNI (`librunanywhere_jni.so`) | Android (min API 24) |\n| Flutter | `bindings/flutter/` | Dart FFI | iOS, Android |\n| React Native | `bindings/react-native/` | NitroModules (JSI HybridObject) | iOS 17.5+, Android arm64 |\n| Web | `bindings/web/` | Emscripten WASM + TypeScript | Chrome, Safari, Firefox |\n| Electron | `bindings/electron/` | Node-API addon | Windows x64 (preview) |\n| Python | `bindings/python/` | pybind11 extension (`runanywhere._core`) | Windows, macOS, Linux (alpha) |\n\nEach has its own `AGENTS.md` with full architecture, build commands, and conventions —\nread it before working in that SDK.\n\n| Native dir | Contents |\n|-----------|----------|\n| `core/` | C/C++ core library: all AI logic, plugin registry, event system |\n| `engines/` | 7 backend plugins: llamacpp, sherpa, onnx, cloud, mlx, qhexrt, neurt |\n| `runtimes/` | 3 runtime adapters: cpu (always), onnxrt, coreml |\n| `idl/` | 39 Protobuf schemas + per-language codegen scripts |\n\n### Consumer apps live in separate repos\n\nThe four full consumer apps were extracted into standalone repos (history preserved); PR\nagainst them there, not here: [runanywhere-ios](https://github.com/RunanywhereAI/runanywhere-ios),\n[runanywhere-android](https://github.com/RunanywhereAI/runanywhere-android),\n[runanywhere-web](https://github.com/RunanywhereAI/runanywhere-web),\n[runanywhere-electron](https://github.com/RunanywhereAI/runanywhere-electron).\n\nTwo example apps remain in-tree (`bindings/flutter/example/`, `bindings/react-native/example/`).\nAll example apps share brand orange `#FF6900`, documented in `docs/DESIGN_GUIDELINE.md`.\n\n### Minimal examples are how you verify an SDK change locally\n\nEach in-repo example consumes its SDK from local source (no staging/publishing needed to\nsee an edit): Swift via `RUNANYWHERE_USE_LOCAL_NATIVES=1`, Kotlin via a Gradle composite\nbuild (`includeBuild` + `dependencySubstitution`), Web via Vite aliases into\n`packages/*/src`. They're deliberately minimal (one prompt in, one streamed completion\nout) — feature-complete UI belongs in the consumer repos above.\n\n---\n\n## Cross-platform architecture\n\n`idl/*.proto` is the schema root; `idl/codegen/generate_all.sh` emits per-language\nbindings (Swift, Kotlin/Wire, TS/ts-proto, Dart), all generated and none committed — see\n[`docs/reference/generated-code-contract.md`](docs/reference/generated-code-contract.md)\nfor the full \"what generates what, when\" contract before touching `idl/`.\n\nAll SDKs reach `runanywhere-commons` through the `rac_*` C API. Commons holds the\ncomponent layer (lifecycle), the service layer (dispatch), and the plugin registry, and\nreaches engines through `rac_engine_vtable_t`.\n\n| Engine | Primitives |\n|---|---|\n| llamacpp | LLM, VLM |\n| sherpa-onnx | STT, TTS, VAD |\n| onnx | Embed, Segment |\n| qhexrt | Hexagon NPU |\n| neurt, cloud | Apple Neural Engine, HTTP |\n\nBackend base priorities (highest wins per primitive via `rac_plugin_find()`, no\nruntime/format scoring): qhexrt=150 (QNN-context models only), mlx=110 (Apple),\nllamacpp=100, sherpa=90, onnx/cloud=50. An explicit engine name is honored through\n`rac_plugin_find_for_engine()` regardless of priority.\n\n### Non-obvious invariants that span every SDK\n\n- **Platform adapter IoC**: `rac_platform_adapter_t` is a flat C struct of function\n  pointers populated by each SDK before `rac_init()`. C++ never calls platform APIs\n  directly — file I/O, HTTP, Keychain, logging, memory queries all pass through it.\n- **Two-phase init**: every SDK does Phase 1 (sync: register adapter, load native libs,\n  configure logging) then Phase 2 (async: authenticate, register device, fetch model\n  assignments, discover downloaded models).\n- **Plugin ABI**: `RAC_PLUGIN_API_VERSION = 9u`; a version mismatch is an immediate\n  rejection. Each vtable has 10 active primitive slots and 7 reserved. NULL slot = not\n  supported. `rerank_ops` (wire value 11) was promoted from `reserved_slot_2` in ABI v8;\n  wire value 6, retired in ABI v4, stays permanently retired — never reuse it. See\n  `core/AGENTS.md` for the full ABI history and how to add a primitive.\n- **Static vs. dynamic plugins**: iOS and WASM force `RAC_STATIC_PLUGINS=ON` (no\n  `dlopen`), registered via `RAC_STATIC_PLUGIN_REGISTER(name)` + `-force_load` /\n  `--whole-archive`. Android/Linux/macOS default to dynamic loading via\n  `rac_registry_load_plugin()`.\n- **Streaming fan-out**: C++ allows only one proto-byte callback per component handle.\n  Each SDK implements its own fan-out to multiplex that into multiple subscribers (Swift\n  `AsyncStream`, Kotlin `Flow`, Dart `StreamController`, TS `AsyncIterable`).\n- **Proto types are canonical**: never hand-write enum values or structured types that\n  exist in `idl/*.proto` — use the generated types/typealiases and regenerate instead.\n- **HTTP is platform-provided**: no libcurl. Each SDK registers a\n  `rac_http_transport_ops_t` (URLSession on Apple platforms, OkHttp on Android,\n  `emscripten_fetch` on Web).\n- **NeuRT crosses a repo boundary as BYTES, not source**: the Apple engine is built and\n  published by the private `neurun` repo and consumed here as prebuilt archives, pinned by\n  tag + SHA-256 in `core/VERSIONS` (`NEURT_*`). Fetch with `scripts/build/download-neurt.sh`.\n  Each archive carries a receipt naming the `RAC_PLUGIN_API_VERSION` it was built against,\n  and both the downloader and CMake refuse a mismatch — a vtable-layout change relinks\n  cleanly and corrupts dispatch at runtime, so nothing else would catch it.\n\nA side-by-side comparison of entry point / bridge / streaming / events / storage / HTTP\nper SDK — useful when porting a fix across SDKs — lives in\n[`docs/reference/cross-sdk-parity.md`](docs/reference/cross-sdk-parity.md).\n\n---\n\n## Building and running\n\nThe root `CMakeLists.txt` (version from `core/VERSION`) is the single entry point for\nnative builds; `CMakePresets.json` defines `macos-{debug,release}`, `linux-{debug,release,asan}`,\n`ios-{device,simulator}`, `android-arm64`, `wasm`, `cpp-desktop-*`, and the `windows-*` presets.\n\n```bash\ncmake --preset macos-debug && cmake --build build/macos-debug && ctest --preset macos-debug\n```\n\n**`./run <group> [subcommand]`** is the unified wrapper for everything else — build,\nlint, and run any SDK or example app from one CLI (`./run --help` for the full menu):\n\n```bash\n./run doctor                    # scan host toolchains, show what's buildable here\n./run setup                     # provision env for every host-buildable target\n./run sdk commons build-android # build C++ commons for all Android ABIs, stage .so files\n./run sdk kotlin build          # (etc. — see per-SDK AGENTS.md for direct tool commands)\n./run example android install   # build + install + launch the Kotlin minimal example\n./run codegen                   # regenerate every language binding from idl/\n```\n\nFor the direct `swift build` / `./gradlew` / `melos` / `yarn` / `npm` commands each SDK\nuses under the hood, and its test/lint/publish targets, see that SDK's own `AGENTS.md`\n(`bindings/<name>/AGENTS.md`) — the commands are kept there, not duplicated here, so they\ncan't drift.\n\nCross-platform build scripts worth knowing about at the root level:\n\n```bash\n./bindings/swift/scripts/build-core-xcframework.sh   # also syncs into RN/Flutter plugin dirs\n./scripts/build/build-core-android.sh                # .so for all ABIs → every SDK's jniLibs/\n./bindings/web/scripts/build-core-wasm.sh\n./scripts/setup/setup-toolchain.sh                   # protoc/wire/ts-proto toolchain\n./idl/codegen/generate_all.sh [--only <lang>]         # swift|kotlin|dart|ts|cpp|python\n```\n\n---\n\n## Version management and releases\n\nCanonical version: `core/VERSION` (single-line semver). Bump everywhere with:\n\n```bash\n./scripts/release/sync-versions.sh <version>\n```\n\nThe full release runbook (version bump → PR → tag → GitHub Release → npm/Maven/pub.dev/\nSwiftPM publishing → cutting the `runanywhere-swift` SPM distribution repo → starter-app\nsmoke tests) is a multi-step process captured in three skills — use them rather than\nimprovising the steps: **sdk-release** (version bump through published GitHub Release),\n**sdk-publish** (registry publishing + the `runanywhere-swift` dist repo cut), and\n**sdk-test-starters** (post-release device/app smoke tests).\n\n### Release pipeline lessons (v0.20.29 through v0.20.33 — read before cutting a release by hand)\n\n- **`native_ios` and `native_android` always rebuild fresh archives on every `release.yml`\n  run — there is no reuse/skip mechanism for either job** (unlike `native_web`'s\n  `reuse_native_web_run_id` or the cpp-desktop kits' `reuse_cpp_desktop_run_id`). This makes\n  the Apple/Android TEMP-pin convention (Package.swift's `sdkVersion`, the Flutter podspecs'\n  `asset_version`, Kotlin's `nativeLibVersion` pinned to an old version \"because this release\n  ships no new iOS/Android archives\") **structurally unsound for any release where those two\n  jobs actually run — which is every release.** It surfaced as three separate bugs\n  (`auto-tag.yml`'s coherence check, `package-sdk.sh`'s podspec check, and `sync-checksums.sh`'s\n  publish-time verification) before the pattern was recognized. If a release genuinely ships\n  no new Apple/Android bytes, that has to be proven by diffing the actual built artifacts\n  against the previous release, not assumed from what changed in the diff.\n- **`RunAnywhereMLXRuntime`'s stripped Mach-O is not byte-reproducible across builds**\n  (`RAC_CHECKSUM_SKIP=RunAnywhereMLXRuntime` in `release.yml`'s publish job exists because of\n  this, and is the ONLY archive exempted from the publish-time checksum gate). The corollary\n  that bit v0.20.29: **the checksum committed to `Package.swift` / the Flutter MLX podspec\n  can only be computed from a build that already happened, but the tag that freezes those\n  files has to be pushed before `native_ios` (which produces that exact artifact) even runs**\n  — so re-syncing checksums against an EARLIER, superseded build (e.g. after a tag had to be\n  moved and re-dispatched to fix an unrelated bug) commits a value that does not match what\n  the FINAL successful run actually publishes, and the skip means nothing catches it. v0.20.29\n  shipped this way (committed `d942788f...`, actually published `fc4b23a0...`) — caught only\n  because the Flutter `runanywhere_mlx` package hadn't been published to pub.dev yet for that\n  version. Before publishing any package that ships `RunAnywhereMLXRuntime`, re-run\n  `bindings/swift/scripts/sync-checksums.sh` against the artifacts from the SPECIFIC run whose\n  tag is being published — never reuse a checksum computed against an earlier attempt.\n- **The Electron QHexRT backend never set `ADSP_LIBRARY_PATH`, and one comment in the\n  codebase confidently asserted Windows doesn't need it.** It does, for exactly one decode\n  path: `QHexRT/src/bonsai/fastrpc_win.cpp`'s custom FastRPC skel (the ternary/Bonsai\n  decoder, e.g. `qwen3.8-27b-1bit-npu`) resolves through `ADSP_LIBRARY_PATH ∪ cwd`, separately\n  from the standard QNN HTP graph path's `LoadLibraryW` + PATH search that `bridge.ts`'s\n  `addSidecarDirToDllSearch` already handled. Every OTHER QHexRT model worked fine in the\n  packaged app, which is exactly what let this ship undetected — validate the Bonsai/ternary\n  path specifically, not just \"some NPU model works.\" Fixed in `bridge.ts`'s\n  `addSidecarDirToDspSearchPath` (#803).\n- **`package-private-engine-overlay.sh`'s QAIRT-runtime copy step filtered on `{\".dll\",\n  \".lib\"}` only**, silently dropping every `.so`/`.cat` skel file (both the standard HTP skel\n  and, since it never read `engines/qhexrt/prebuilt/current/dsp/win-arm64/` at all, the\n  Bonsai skel too). This meant **RCLI could not have run any QHexRT model** — standard or\n  ternary — from a fresh private overlay, a gap invisible until someone actually tried to run\n  inference rather than just building. `bindings/electron/scripts/bundle-native.ts` already\n  had the correct pattern (copy `dsp/win-arm64/` flat, no extension filter); the overlay\n  script just never matched it. Fixed in #804.\n- **A new NeuRT archive must be added in FOUR places, and three of them only fail in a build\n  configuration your Mac never produces.** v0.20.33 added `libneurt_rac_tts_ops.a` and took two\n  wasted release candidates to land, because each site fails somewhere different and late:\n  `engines/neurt/CMakeLists.txt` (probe list + imported target + `_neurt_extra_links`),\n  `build-core-xcframework.sh` (pre-flight guard AND `_NEURT_ROUTABLE_ARCHIVES` — two lists in one\n  file), `package-private-engine-overlay.sh`, and the `#if RAC_NEURT_ROUTABLE` guard around the\n  vtable entry in `rac_plugin_entry_neurt.cpp`. Miss the force-load lists and every native job goes\n  green while `validate_consumer_flutter` dies an hour later on `Undefined symbol:\n  _g_neurt_tts_ops` — the engine registers, all CMake targets compile, and only a CONSUMER's link\n  fails. Miss the vtable guard and the routable build is fine while every PR job WITHOUT\n  `NEURUN_TOKEN` fails, because a non-routable shell still references a symbol nothing defines.\n  A dev machine with the payload staged is routable, so it reproduces neither. `libneurt_rac_*.a`\n  archives now fail packaging by name if they are not in the list\n  (`neurt_assert_no_unlisted_archives`); the vtable guard has no such mechanism — audit all ten\n  slots when adding one. Reproduce a non-routable build by moving `core/third_party/neurt` aside.\n- **`reuse_native_web_run_id` is SAME-VERSION-ONLY, whatever the diff looks like.** `racommons.wasm`\n  embeds `RAC_VERSION_STRING` and `validate_public_packages.py` asserts it against the version being\n  packaged, so reusing ANY prior release's run fails `sdk_web` with *\"does not embed\n  RAC_VERSION_STRING\"*. Worse than a red job: `native_web` is skipped, so that candidate never\n  uploads a web artifact and can never serve as `publish_from_run_id`. Only a re-dispatch at the\n  same version qualifies.\n- **Verify checksums by NAME as well as by URL — the manifests use two different shapes.** The root\n  `Package.swift` interpolates `\\(sdkVersion)` into each URL; the Flutter manifests bind a checksum\n  to a `name:` through a helper with no URL at the call site at all. A URL-only cross-check silently\n  validates 9 of 15 sites and reports success. Re-hash every site against the zip it names, and\n  remember `RAC_CHECKSUM_SKIP` hides `RunAnywhereMLXRuntime` from `publish` entirely, so the Flutter\n  MLX podspec is verifiable only by hand.\n- **A green `rcli backends --json` proves nothing against the OSS kit.** NeuRT ships as a separate\n  private pack, so the public bottle lists no `neurt` at all — a routability check there passes\n  cleanly and means nothing. Apply the private overlay\n  (`cpp-desktop-macos-arm64-neurt-private` workflow artifact) before believing the result.\n- **General pattern across all four of the above**: each one was invisible to the checks that\n  existed (a version-coherence check, a checksum-skip escape hatch, a \"the model loaded\"\n  smoke test, a \"the build succeeded\" CI job) because none of those checks actually ran the\n  specific thing that was broken. When validating a release, run the SPECIFIC flagship/hardest\n  model end-to-end on-device, not just confirm the pipeline completed.\n- **The RACommons/RunAnywhereMLXRuntime checksum-staleness bug above recurred on v0.20.30 AND\n  v0.20.31** — same `mismatch: RACommonsBinary` failure in `publish`, same root cause both\n  times: the version-bump PR ran `sync-versions.sh` and merged/tagged directly, skipping the\n  **mandatory** \"dispatch a release candidate off the unmerged branch, sync checksums from\n  its real build, THEN merge\" sequence that the **`sdk-release` skill's step 2-3 already\n  document in detail** (including this exact failure signature). Documenting the lesson in\n  prose here was not enough to stop it recurring twice more — **if you are about to run\n  `scripts/release/sync-versions.sh` for ANY reason, including as one step of a larger\n  cross-repo release cascade, invoke the `sdk-release` skill and follow it from step 1**,\n  rather than reconstructing the sequence from memory or treating a \"small\" version bump as\n  exempt. v0.20.31 additionally caught `RunAnywhereMLXRuntime` in the Flutter podspec\n  independently stale in the SAME commit — `RAC_CHECKSUM_SKIP` exempts it from `publish`'s\n  check, so it would have shipped broken to Flutter consumers with no CI signal at all; check\n  every row of the checksum-location table, not only the one the CI error names.\n- **QHexRT's `qhx_runtime_device()` (arch/soc lookup, needed to pick the `v75`/`v79`/`v81`\n  manifest directory in `session_open()`) shared its live-device cost with the standard QNN\n  graph path**, so a `host_only` model with ZERO QNN graphs — the Bonsai/Maple ternary decoder\n  (`qwen3.8-27b-1bit-npu`) — paid for a live QNN HTP device anyway, which then contended with\n  the decoder's own direct FastRPC session for the same Hexagon cDSP and made every generation\n  fail with a bare `HostOpFailed`. This shipped in v0.20.30 (the SAME release that fixed the\n  `ADSP_LIBRARY_PATH` bug for the same model) and was found only by running that SPECIFIC model\n  on real Windows-ARM64 hardware after the \"fix\" — reinforcing the point above: a green\n  `HostOpFailed`-free run of a DIFFERENT QHexRT model proves nothing about this one. Two red\n  herrings were chased and ruled out with live device tracing before the real cause: neither\n  `ADSP_LIBRARY_PATH` (confirmed correctly set) nor a QAIRT-version mismatch between the\n  shipped host DLLs and the on-device skel (confirmed present, but fixing it alone did not fix\n  the failure) was the actual cause. Fixed in `neurun` v0.20.31 (`Backend::profile()` now uses\n  a separate, device-handle-free `deviceGetPlatformInfo` query instead of sharing\n  `ensure_device()` with `device()`) + this repo's `qhexrt_session.cpp` (#810, a complementary\n  fix removing a second, redundant eager device query used only for a log line).\n\n---\n\n## CI/CD workflows (`.github/workflows/`)\n\n| Workflow | Trigger | Purpose |\n|----------|---------|---------|\n| `pr-build.yml` | PR to main, push to main/feat branch | Parallel native builds (macOS/Linux/iOS/Android) + per-SDK typecheck + centralization/coherence gates |\n| `release.yml` | Tag `v*.*.*` or manual | Full artifact build matrix, SDK packaging, consumer validation, draft Release |\n| `auto-tag.yml` | PR merged to main with `release:*` label | Verifies the reviewed semver bump, pushes that exact git tag |\n| `idl-drift-check.yml` | Changes to `idl/` or generated files | Regenerates protos, verifies schema lock + untracked generated trees |\n| `legacy-files-blocklist.yml` | All PRs/pushes | Prevents specific deleted files from being re-introduced |\n| `secret-scan.yml` | PRs and pushes to main | Incremental gitleaks scan on diff range |\n| `check-no-pii-logging.yml` | PRs/pushes to main/master/feat-branch | Guards against logging signed URLs alongside active-download destination paths |\n\n---\n\n## Platform requirements\n\n| Platform | Min version | Build tool | Key versions |\n|----------|------------|------------|--------------|\n| iOS / macOS | 17.5 / 14.5 | Xcode 26+ | Swift 6.2 |\n| Kotlin SDK | Android API 24 | AGP 9.2.1 / Gradle 9.5.0 | Kotlin 2.4.0, NDK 27.3.13750724 |\n| Flutter | 3.44.6 | Melos / AGP 9.0.1 / Gradle 9.1.0 | Dart 3.12.2+, NDK 28.2.13676358 |\n| React Native | 0.85.3 (min 0.83.1) | Yarn Berry 3.6.1 | NitroModules, Hermes |\n| Web | Chrome 86+ | Vite | Emscripten 6.0.2, Node 24 LTS |\n| C++ core | N/A | CMake 3.24+ | C++20, Ninja |\n\n---\n\n## Non-obvious configuration details\n\n- **NDK pin**: `core/VERSIONS::NDK_VERSION` (27.3.13750724) is the single source of truth,\n  mirrored into `bindings/kotlin/gradle.properties`. NDK 27 (r27d) gives 16 KB\n  page-alignment required by Android 15+ — NDK 25.x's 4 KB-aligned `libc++_shared.so` /\n  `libomp.so` trips Android 16's page-size enforcement. Mirror this pin whenever bumping\n  it; Flutter/RN Android build files carry their own fallback literals that can drift.\n- **`useLocalNatives`** (Kotlin `gradle.properties`, similarly named flags elsewhere):\n  `true` builds native libs from source locally; CI and most non-local runs set it\n  `false` to download prebuilt `.so`/`.xcframework`/`.wasm` from GitHub Releases instead.\n- **Web SDK** has several hard-won WASM/browser runtime workarounds (VLM worker crash\n  recovery, a Qwen2-VL WebGPU NaN bug, cross-origin isolation requirements) — see\n  [`docs/reference/web-runtime-gotchas.md`](docs/reference/web-runtime-gotchas.md).\n- **Cloud/Linux dev VM** (e.g. Cursor Cloud): what's buildable there and the\n  clang/NDK/KVM gotchas are in\n  [`docs/reference/cursor-cloud-environment.md`](docs/reference/cursor-cloud-environment.md).\n\n---\n\n## Pre-commit hooks\n\n```bash\npre-commit run --all-files                     # everything\npre-commit run ios-sdk-swiftlint --all-files   # SwiftLint only\n```\n\nConfigured hooks: gitleaks (secrets), trailing-whitespace, end-of-file-fixer, check-yaml,\ncheck-added-large-files (1000 KB max), check-merge-conflict, object file detection,\nSwiftLint (SDK + example app), periphery (unused Swift code detection).\n\n---\n\n## Where to look for current in-flight work\n\n`thoughts/shared/plans/` holds active/completed execution plans (including any in-flight\nSDK reorg or migration briefs — e.g. the Electron reorganization plan); `thoughts/shared/issues/`\nholds tracked bug write-ups. Check these directories directly rather than trusting a\nstatus summary written into this file — both change too often for a static snapshot here\nto stay accurate.\n",".claude/skills/telemetry-e2e/SKILL.md":"---\nname: telemetry-e2e\ndescription: Prove that what an SDK binding EMITS on the telemetry wire is correct and that it actually arrives — vocabulary compliance, presence/absence semantics, device-id stability, and a received request body / stored row as evidence. Use when touching telemetry_json.cpp, telemetry_manager.cpp, rac_telemetry_vocabulary.h, any binding's platform-adapter telemetry bridge, or when asked to verify a telemetry field isn't silently dropping.\n---\n\n# Telemetry Wire Correctness (SDK side)\n\nThis is the SDK-side counterpart to the backend's `telemetry-e2e` skill (private\n`Runanywhere-monorepo` repo — proves event-to-stored-row on the server side). This one\nproves the other half: that commons *serializes* a call correctly and that the bytes\n*leaving* a binding are the bytes a real backend accepts. It does not cover per-starter-app\ninference smoke tests — see `sdk-test-starters` for that.\n\n## 0. The evidence bar\n\nCompiling is not evidence. A green `telemetry_extraction_tests` run is not evidence that a\nreal binding's real call site reaches the wire correctly — it only proves the *serializer*\nhandles a hand-built payload correctly. In order of increasing strength:\n\n1. **Unit test green** (`ctest -R telemetry_extraction_tests`) — proves the JSON builder's\n   presence/vocabulary rules are still correct in isolation. Necessary, not sufficient.\n2. **Received request body** — capture the actual JSON `rcli` (or a real binding) put on\n   the wire and read it: right modality endpoint, right keys, no vocabulary values that\n   got through that shouldn't have, measured zeros kept as `0` not `null`.\n3. **Stored row matches, field-for-field** — the backend's own ingest response\n   (`events_received`/`events_stored`/`events_skipped`) or a direct DB read shows the row\n   landed with the values you actually sent, not just a 2xx. This is the only tier that\n   also proves the vocabulary/schema on both sides still agree.\n\nA PR description that says \"sent telemetry, no errors\" without quoting a request body or a\nstored-row count is not evidence of anything except that a socket didn't error out.\n\n## 1. The emit path\n\nEvery binding funnels through the same two C ABI entry points in\n`core/src/infrastructure/telemetry/telemetry_manager.cpp`:\n\n- `rac_telemetry_manager_track` — flat-struct payload (`rac_telemetry_payload_t`), used by\n  `rcli`'s `telemetry emit`/`telemetry blast` and by hand-built events.\n- `rac_telemetry_manager_track_proto` — the real path every language binding actually uses:\n  a binding builds a protobuf `SDKEvent` (`sdk_events.pb.h`) at the real call site (e.g. an\n  LLM `generate()` completing), and `proto_event_type_string()` + the per-domain extraction\n  blocks in `telemetry_manager.cpp` turn it into the same flat payload. `framework_proto_to_string()`\n  / `framework_to_string()` / `clean_framework()` do the backend-name normalization here —\n  this is where a raw enum like `INFERENCE_FRAMEWORK_LLAMA_CPP` becomes the wire string `\"llamacpp\"`.\n- `stamp_live_device_state()` (same file) then overwrites the SDK-origin/device-state fields\n  (`sdk_binding`, `platform`, `battery_state`, memory, core count) from manager-owned state,\n  not from whatever the caller passed — a binding cannot inject a platform or binding value.\n- The payload is handed to `rac_telemetry_manager_payload_to_json()` in\n  `core/src/infrastructure/telemetry/telemetry_json.cpp`, which does field-by-field\n  serialization (see §2/§3 for the two contracts enforced there).\n- `send_batch_json()` batches by modality and POSTs to `/api/v2/sdk/telemetry/{modality}`\n  through whichever `rac_http_transport_ops_t` the binding registered (URLSession/OkHttp/\n  `emscripten_fetch`/curl for `rcli` — see the root `AGENTS.md`'s \"HTTP is platform-provided\"\n  rule). There is no `libcurl` fallback baked into commons itself.\n\nIf a field is going missing on the wire, the fix is almost always in\n`telemetry_json.cpp` (a field never wired into the serializer) or in the call site's proto\npopulation (a field never set on the `SDKEvent`) — not in a binding's bridge code, which\njust forwards bytes.\n\n## 2. The closed vocabularies — and the silent-drop rule\n\n`core/include/rac/infrastructure/telemetry/rac_telemetry_vocabulary.h` is a **generated\nfile** (see §5) holding six closed tables, each with a `RAC_TELEMETRY_<NAME>_COUNT` and a\n`RAC_TELEMETRY_<NAME>_VALUES[]` array:\n\n| Table | Count | Wire field | Values |\n|---|---|---|---|\n| `RAC_TELEMETRY_EVENT_TYPE` | 106 | `event_type` | namespaced strings, e.g. `llm.generation.completed`, `auth.succeeded`, `stt.transcription.completed` |\n| `RAC_TELEMETRY_BACKEND` | 22 | `backend` / `framework` | `onnx, sherpa, llamacpp, foundation_models, system_tts, fluid_audio, coreml, whisperkit_coreml, mlx, qhexrt, neurt, tflite, executorch, mediapipe, mlc, pico_llm, piper_tts, swift_transformers, cloud, builtin, none, unknown` |\n| `RAC_TELEMETRY_MODEL_SOURCE` | 3 | `model_source` | `catalog, builtin, user` |\n| `RAC_TELEMETRY_PLATFORM` | 9 | `platform` | `ios, android, macos, windows, linux, web, tvos, watchos, visionos` |\n| `RAC_TELEMETRY_SDK_BINDING` | 9 | `sdk_binding` | `swift, kotlin, flutter, react-native, web, electron, python, cli, cpp` |\n| `RAC_TELEMETRY_BATTERY_STATE` | 4 | `battery_state` | `charging, full, unplugged, unknown` |\n\n**The rule, verbatim from `add_vocabulary_string()` in `telemetry_json.cpp`:** if a value is\nnon-null but not present in its table, the field is dropped to `null` and the *rest of the\nevent still ships*. This is by design (a bad `backend` string must not cost the whole\nobservation) but it means:\n\n- The backend never sees the bad value, never 422s, never quarantines anything — there is\n  no error path anywhere for this. A field just isn't there.\n- The only client-side signal is a `RAC_LOG_WARNING(\"Telemetry\", \"Dropping %s: '%s' is not\n  in the published vocabulary\", ...)` log line — easy to miss unless you're specifically\n  watching debug logs.\n- `platform`, `sdk_binding`, and `battery_state` are stamped by the manager itself (§1), not\n  supplied by a binding's call site, so in practice only `backend`/`framework` (from the\n  engine/model layer) and `event_type` (only reachable via the proto path, checked at CI\n  time — see §5) are realistic places a new value can silently disappear.\n- `test_telemetry_extraction.cpp`'s vocabulary block (`\"vocab-1\"`/`\"vocab-2\"` cases) is the\n  concrete regression guard: it sends `framework = \"LlamaCpp\"` (a real spelling that used to\n  reach production) and asserts the string never appears on the wire while `model_id` still\n  does. If you touch `add_vocabulary_string()` or add a new vocabulary-gated field, extend\n  that test rather than trusting a manual check.\n\nHistorical motivation (from the header's own comment): before this table existed, stored\nproduction rows had four spellings of `llama.cpp`, three of ONNX, and binding names like\n`\"react-native\"` recorded as a `platform` — this file exists specifically to make that class\nof drift fail in this repo instead of being discovered in a database nobody was watching.\n\n## 3. Presence vs. absence: the \"measured zero\" contract\n\nBefore the fix documented in `telemetry_json.cpp`'s header, extractors read proto3 scalar\nfields directly — which report `0` for a field that was never set — and the old serializer\n(`add_int`/`add_double`) *skipped* zero values to keep payloads small. Result: a genuinely\nmeasured `output_tokens = 0` (an empty completion) and an unmeasured `tokens_per_second`\nboth arrived on the wire as absent, indistinguishable from each other.\n\nThe fix is `has_x`-gated serialization. Two helper families in `telemetry_json.cpp`:\n\n- `add_int_or_null(key, value, is_valid)` / `add_double_or_null(key, value, is_valid)` —\n  emit the real value (including `0`) when `is_valid` is true, else the JSON literal `null`.\n  Non-finite doubles (`NaN`/`Inf`) always degrade to `null` regardless of `is_valid`, because\n  JSON has no such literal and one bad value must not corrupt the whole batch.\n- The `is_valid` argument is a real `has_x`/sentinel check per field, not a blanket flag —\n  e.g. `payload->has_processing_time_ms != RAC_FALSE`, `payload->total_memory > 0`,\n  `payload->battery_level >= 0`.\n\n**What to check to confirm this stayed fixed** (this is exactly what\n`test_telemetry_extraction.cpp`'s presence block does — see §4): send an LLM completion\nevent with `output_tokens = 0` and `input_tokens = 0` explicitly set, and\n`tokens_per_second`/`context_length` left unset. Assert the wire body contains\n`\"output_tokens\":0` and `\"input_tokens\":0` as literal numbers, and `\"tokens_per_second\":null`\n/ `\"context_length\":null` — not all four as `null`, and not all four as `0`. If a future\nrefactor reverts to plain `add_int`/`add_double` for any of these fields, that regression\ntest is the thing that catches it; grep `telemetry_json.cpp` for `add_int(` / `add_double(`\n(the non-`_or_null`/non-`_always` forms) if you suspect a field regressed — those two still\nsilently skip zero by design and must never be used for a field where `0` is meaningful.\n\n## 4. Build, test, and what each telemetry test asserts (macOS)\n\n```bash\ncmake --preset macos-debug\ncmake --build build/macos-debug\nctest --preset macos-debug --output-on-failure -R 'telemetry|device_identity'\n```\n\nOr the non-preset form (`core/AGENTS.md`'s documented path):\n\n```bash\ncmake -B build -DRAC_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Debug\ncmake --build build\nctest --test-dir build --output-on-failure\n```\n\n`telemetry_extraction_tests` and `device_identity_tests` are registered in\n`core/tests/CMakeLists.txt`; `rcli_telemetry_live_tests` is registered separately in\n`rcli/tests/CMakeLists.txt`. All three are hand-rolled assertion runners — `CHECK()` in\nsome files, `ASSERT_EQ`/`ASSERT_TRUE` (from `core/tests/test_common.h`) in others — never\nGoogleTest:\n\n| ctest name | Binary | What it asserts |\n|---|---|---|\n| `telemetry_extraction_tests` | `test_telemetry_extraction` | Full proto→JSON pipeline per modality (LLM token counts/`tokens_per_second`, STT NaN-confidence never leaks as the string `\"nan\"`, TTS `characters_per_second`, embeddings `total_tokens`/`batch_size`/`embedding_dimension`, LoRA failure-path fields, RAG retrieval counts, VLM image/timing fields, live device-state stamping); the presence/measured-zero contract (§3); the closed-vocabulary drop rule (§2); and an HTTP-retry backoff (a failed batch is re-sent only after >5s, never before). Requires `RAC_HAVE_PROTOBUF` — the test binary itself no-ops with exit 0 if commons wasn't built with protobuf support. |\n| `device_identity_tests` | `test_device_identity` | Every branch of `rac_device_get_or_create_persistent_id()`'s resolution chain (§6) against a mocked platform adapter — cache hit, vendor-ID fallback, fresh-UUID generation, and that a `secure_set` failure doesn't silently drop the ID. |\n| `rcli_telemetry_live_tests` | `test_rcli_telemetry_live` | **Opt-in, always registered but a no-op by default.** Runs only with `--live` *and* `RUNANYWHERE_BASE_URL`/`RUNANYWHERE_API_KEY` set; otherwise prints \"skip\" and exits 0 — a green ctest run for this target proves nothing by itself. When live, it runs `rcli::bootstrap()` (real API-key → device-register → JWT handshake), sends one real event per modality (LLM, embeddings, RAG, VLM, LoRA-failure) through the real HTTP transport, and fails on any non-2xx response — this is the test that would catch a strict-schema `422 extra_forbidden` from field drift against the real V2 endpoints. |\n\nRun it live (§7 has the full local-backend flow):\n\n```bash\nRUNANYWHERE_BASE_URL=https://<backend-origin> RUNANYWHERE_API_KEY=<key> \\\n  ./build/macos-debug/rcli/tests/test_rcli_telemetry_live --live\n```\n\n`test_platform_bridge` (same `CMakeLists.txt`, adjacent target) is the wider companion\ncheck — \"every value a binding supplies must reach the wire\" — covering the platform half\nof the same two flat C structs (`rac_client_info_t`/`rac_device_registration_info_t`) for\nevery SDK at once; run it alongside if you're touching client-info fields rather than\ntelemetry event fields specifically.\n\n## 5. Regenerating the vocabulary header, and the drift gate\n\n`rac_telemetry_vocabulary.h` (§2) is generated from `idl/http/sdk-openapi.json` — the\ndevice-facing OpenAPI subset copied in from the private backend repo. **How that file gets\nupdated on the backend side, and how it crosses into this repo, is the monorepo's\n`openapi-contract` skill's job — read that by name in the other repo rather than\nre-deriving the steps here.** From inside this repo, once `idl/http/sdk-openapi.json` is\ncurrent:\n\n```bash\npython3 idl/codegen/generate_telemetry_vocabulary.py          # regenerate the header\npython3 idl/codegen/generate_telemetry_vocabulary.py --check  # CI drift check; exit 1 if stale\n```\n\n`.github/workflows/telemetry-vocabulary-drift.yml` runs three checks on every push to\n`main`/`development` and on any PR touching `idl/http/**`, the codegen script, or the\ntelemetry source/header dirs:\n\n1. **Generated header is current** — the `--check` command above.\n2. **Every event type the core can emit is in the vocabulary** — a Python check that parses\n   `proto_event_type_string()`'s body out of `telemetry_manager.cpp` via regex, unions in\n   the two prefix-expansion families (`stt.model.*`/`tts.voice.*`/`llm.model.*` and\n   `llm*`/`vlm*` generation suffixes), and diffs against `TelemetryEventType`'s enum in\n   `idl/http/sdk-openapi.json`. Anything emittable but unpublished fails the job.\n3. **Every framework string the core can emit is in the vocabulary** — same technique\n   against `framework_proto_to_string()` and `framework_to_string()` vs. `TelemetryBackend`.\n\n**A documented drift you will hit if you trust the wrong file**: `idl/http/README.md`'s own\n\"What it pins\" table names the schema for the `framework`/`backend` field as\n`TelemetryFramework` — that's stale. The actual generator\n(`idl/codegen/generate_telemetry_vocabulary.py`'s `VOCABULARIES` dict) and the header it\nproduces both say `TelemetryBackend`. Trust the generator and the generated header's own\n`// Published as X` comments over that README table.\n\n## 6. Per-binding device identity: where it's created, where it's persisted\n\nThe resolution chain itself is centralized in commons —\n`core/src/infrastructure/device/device_identity.cpp`\n(`rac_device_get_or_create_persistent_id`, tested by `device_identity_tests`, §4) — and is\nthe same fixed order for every binding:\n\n1. `adapter.secure_get(\"com.runanywhere.sdk.device.uuid\")` — if found and non-empty, use it,\n   full stop.\n2. `adapter.get_vendor_id` (if the platform adapter provides one) — use it, then persist it\n   via `secure_set` so step 1 hits next boot.\n3. Generate a fresh RFC-4122 v4 UUID, persist it via `secure_set`.\n\nWhat differs per binding is **only** the `secure_get`/`secure_set` backing store — an\nunstable backing store (one that doesn't survive a real app restart, or that resolves to a\ndifferent physical location than the read path expects) is exactly the bug class that\ninflates device counts, because step 3 fires again on every \"cache miss\":\n\n| Binding | Backing store | Where |\n|---|---|---|\n| Swift (iOS/macOS) | Keychain, service `com.runanywhere.sdk` | `bindings/swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+PlatformAdapter.swift` (`platformSecureGetCallback`/`platformSecureSetCallback`) |\n| Kotlin (Android) | Android Keystore AES-256-GCM key, ciphertext in a no-backup app file (not `SharedPreferences`) | `bindings/kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/security/AndroidKeychainManager.kt` (`NoBackupCiphertextStore`), wired through `CppBridgePlatformAdapter.PlatformSecureStorage` |\n| Flutter | Native Keychain/Keystore helper via FFI symbols `ra_flutter_secure_storage_{store,retrieve,delete}` | `bindings/flutter/packages/runanywhere/lib/native/dart_bridge_secure_storage.dart`; the device-UUID key constant is duplicated (deliberately, per its own comment) as `_keyDeviceUUID = 'com.runanywhere.sdk.device.uuid'` in `dart_bridge_device.dart` |\n| React Native (Android) | `SecureStorageManager` (native Android side) via `PlatformAdapterBridge.kt`'s `secureGet`/`secureSet` | `bindings/react-native/packages/core/android/src/main/java/com/margelo/nitro/runanywhere/PlatformAdapterBridge.kt` |\n| Web | `localStorage`, key `rac_sdk_plaintext_com.runanywhere.sdk.device.uuid` — **plaintext**, no OS-level encryption equivalent exists in a browser; restricted by convention to non-sensitive IDs | `bindings/web/packages/core/src/runtime/PlatformAdapter.ts` (`registerSecureGet`/`registerSecureSet`); also has a separate `stableVendorId()` fallback under key `rac_sdk_plaintext_vendor_id` |\n| Electron (macOS/Linux) | Flat file under a secure directory, path-traversal-guarded (`fs::path(key)` must not be absolute or contain `..`) | `bindings/electron/native/posix_platform_adapter.cpp` (`posix_secure_get`/`posix_secure_set`) |\n| Electron (Windows) | Windows DPAPI (`CryptProtectData`, current-user scope, no UI) | `bindings/electron/native/win32_platform_adapter.cpp` (`win_secure_get`/`win_secure_set`) |\n\nThe key string itself — `com.runanywhere.sdk.device.uuid` — is intentionally the same\nliteral across Swift, Flutter, and RN (each source comments cross-reference the other two by\nname), so grepping for it is a fast way to confirm a new binding is reading/writing the\n*same* key it thinks it is, not a typo'd sibling.\n\n**How to verify stability across a real restart** (not just \"the code looks right\"):\n\n1. Run the binding once, capture the emitted `device_id` from a real request body (§7) or\n   from `POST /api/v1/devices/register`'s response.\n2. Fully terminate the process (not just background it — on mobile this matters, a\n   backgrounded app can keep its in-memory device-id cache alive without ever touching\n   the read path again).\n3. Relaunch and repeat step 1.\n4. The `device_id` must be byte-identical. If it changed, the break is almost always one of:\n   the backing store not surviving process death (e.g. an in-memory-only stub used during\n   development), a key-name mismatch between the write and read paths, or (Android\n   specifically) `Context.deleteSharedPreferences` being invoked on the wrong file — see\n   `AndroidKeychainManager.kt`'s constructor, which deletes a hardcoded *legacy* prefs name\n   on every construction; if that name were ever widened to match the real store, every\n   Android launch would look like a fresh device.\n\nTwo-in-one launches on the *same physical device* (e.g. two emulator instances, or a\nsame-device reinstall vs. a real fresh install) are a different, real-world failure mode\nfrom a code bug — don't conflate an intentional reinstall's fresh ID with instability.\n\n## 7. Point a local build at a local backend and prove an event landed\n\n`rcli` is the fastest way to exercise the full wire path without a mobile/browser\ntoolchain — it links `rac_commons` directly and speaks real HTTP via curl.\n\n```bash\ncmake --preset rcli-macos-release   # or rcli-linux-release on Linux\ncmake --build build/rcli-macos-release --target rcli -j \"$(sysctl -n hw.logicalcpu)\"\n```\n\n**Keyless (development environment) — no API key, works against any backend that allows\nanonymous V2 telemetry:**\n\n```bash\n./build/rcli-macos-release/rcli/rcli --environment development \\\n  --base-url http://127.0.0.1:8000 \\\n  telemetry blast --processing-ms 42.5 --session-id my-check \\\n  --input-tokens 128 --output-tokens 256\n```\n\nThis is exactly the shape `scripts/ci/oss_keyless_telemetry_blast.sh` runs in CI against the\npublic staging backend — `--environment development --base-url <origin> telemetry blast\n...` with no `--api-key`. `telemetry blast` sends one representative event per modality (all\n12) and prints a `MODALITY | RESULT | STATUS | RECEIVED | STORED | SKIPPED` table parsed\ndirectly from the backend's own batch response — **the `STORED` column is authoritative\nserver-side confirmation**, not just an HTTP 2xx. `run_telemetry_blast()`'s own exit-code\nlogic (`rcli/src/commands/cmd_telemetry.cpp`) does fold `stored >= count` into its overall\npass/fail, so a genuine storage shortfall on any modality does make the process exit\nnon-zero, not just a transport error — but the exit code is one bit for all 12 modalities\ncombined, so a non-zero exit still requires reading the table to know *which* modality\nfailed and *why* (a `network error` status is a transport problem; an `HTTP 200` row with\n`STORED` under `RECEIVED` is a row-level rejection, e.g. a vocabulary value the backend\ndidn't recognize — a real bug to chase, not the same failure class at all).\n\n**Authenticated (production shape) — for exercising the login handshake too:**\n\n```bash\n./build/rcli-macos-release/rcli/rcli --environment production \\\n  --base-url https://<backend-origin> --api-key <key> \\\n  auth login\n./build/rcli-macos-release/rcli/rcli --environment production \\\n  --base-url https://<backend-origin> --api-key <key> \\\n  telemetry emit --modality llm --count 3 --input-tokens 128 --output-tokens 256\n```\n\n`auth login` only has a path in `production` — `development`'s keyless mode has no login\nstep by design (see `rcli/src/commands/cmd_auth.cpp`'s own subcommand help text).\n\n**To inspect the actual request body**, not just the summary table, add `--json` for\nmachine-readable CLI output and `-v` for debug logging (the debug log includes the\nserialized JSON per batch before it goes on the wire) — or point `--base-url` at a throwaway\nlocal capture endpoint (any script that logs the raw POST body) before pointing it at a real\nbackend, to sanity-check field shape offline first.\n\n**If you have the private backend repo checked out locally too**, running it yourself\n(`cd backend && uv run uvicorn main:app --reload`, per that repo's own README) and pointing\n`--base-url` at `http://127.0.0.1:8000` gets you the strongest possible evidence: query the\nSQLite/Postgres row directly after a blast and diff it field-for-field against what you sent\n— the monorepo's own `telemetry-e2e` skill (its integration suite, hermetic pytest fixtures,\nand `test_telemetry_conformance.py`'s vocabulary/presence/identity assertions against stored\nrows) is the authoritative reference for that side; don't re-implement its DB assertions\nhere, just point at it.\n\n## Evidence bar, restated\n\n- Compiling `rcli` or a binding: **not evidence**.\n- `ctest -R telemetry_extraction_tests` green: **not evidence of a real call site** — only\n  of the serializer in isolation.\n- `rcli telemetry blast` exiting 0: real signal (its exit code does fold in the backend's\n  `stored >= count` check, §7) but still worth pairing with the per-modality\n  `STORED`/`SKIPPED` table in your report — the exit code can't tell you *which* modality\n  or *why* a shortfall happened.\n- A captured request body showing the right endpoint, the right keys, correct\n  presence/absence per §3, and no off-vocabulary values per §2 — **evidence of correct\n  emission**.\n- That request body's values matching a stored row (via the backend's own\n  `events_stored` count at minimum, a direct DB read at best) — **evidence of correct\n  end-to-end delivery**, the only tier worth reporting as \"telemetry works.\"\n",".claude/skills/sdk-publish/SKILL.md":"---\nname: sdk-publish\ndescription: Manually publish an already-tagged, already-released (non-draft GitHub Release) RunAnywhere SDK version to every package registry (npm, Maven Central, pub.dev, Swift SPM dist repo) plus macOS rcli notarization, with heavy verification that nothing fake/stubbed/broken ships.\n---\n\n# SDK Publish\n\nRepo: `~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks` (verify with `pwd`/`git remote get-url origin` — do not assume this path is still correct, checkouts move). Version source of truth: `core/VERSION`. Throughout, `$VERSION` means the bare semver being published (e.g. `0.20.24`), never prefixed with `v` except in git tags / GitHub Release names.\n\n## THIS PROCESS IS INTENTIONALLY MANUAL\n\n**`release.yml` never publishes anywhere public.** It only builds artifacts, packages them, and creates a draft/published **GitHub Release**. It does not call `npm publish`, does not upload to Maven Central, does not call `flutter pub publish`, and does not push the Swift dist repo. Verify this is still true before trusting it — greps below should all return **zero** matches (grep the whole file, not a snippet):\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks\ngrep -inE \"npm publish|publishReleasePublication|publishAllPublicationsToMavenCentral|flutter pub publish\" .github/workflows/release.yml\n```\n\nThis is a **deliberate safety gate**, not a gap. Every step below is a human (or an agent acting on a human's explicit go-ahead) pushing a button that CI intentionally never pushes. Do not propose wiring any of this into CI as a \"fix\" — that would remove the gate.\n\n## Precondition check (always run first)\n\nRefuse to proceed if the release is a draft or the asset set looks incomplete.\n\n```bash\ngh release view v$VERSION --json isDraft,tagName --jq '{isDraft, tagName}'\n# isDraft MUST be false.\n\ngh release view v$VERSION --json assets --jq '.assets[].name' | sort\n```\n\nCross-reference the listing against the `sdk-release` skill's step 8 asset-count expectations (`~/.claude/skills/sdk-release/SKILL.md`). As a second, independent check, also diff against the **previous** tagged release's asset list — same count, same name patterns, only the version token differs:\n\n```bash\ngh release view v<PREVIOUS_VERSION> --json assets --jq '.assets[].name' | sed \"s/<PREVIOUS_VERSION>/$VERSION/\" | sort >/tmp/expected.txt\ngh release view v$VERSION --json assets --jq '.assets[].name' | sort >/tmp/actual.txt\ndiff /tmp/expected.txt /tmp/actual.txt\n```\n\nAs of the last verified run (**v0.20.25, 65 assets** — up from 55 at v0.20.24 because the 5 public Electron tarballs and their sidecars joined the release; see §6), the full set is: `MANIFEST.txt`; per-platform `RACommons-{android-arm64-v8a,android-armeabi-v7a,android-x86_64,ios,linux-x86_64,web,windows-x64}-v$VERSION.{zip,tar.gz}` + `.sha256`; iOS backend zips `RABackend{LLAMACPP,MLX,NeuRT,ONNX,Sherpa}-ios-v$VERSION.zip` + `.sha256`; `RunAnywhereMLX{Metal,Resources,Runtime}-ios-v$VERSION.zip` + `.sha256`; `rcli-{linux-x86_64,macos-arm64,windows-x86_64}-v$VERSION.tar.gz`/`.zip` + `.sha256`; the 8 npm tarballs `runanywhere-{core,llamacpp,mlx,onnx,proto-ts,web,web-llamacpp,web-onnx}-$VERSION.tgz` + `.sha256`; the **5 Electron npm tarballs** `runanywhere-electron{,-llamacpp,-onnx,-sherpa,-neurt}-$VERSION.tgz` + `.sha256` (new as of v0.20.25); and `runanywhere-kotlin-maven-v$VERSION.zip` + `.sha256`. There is **no** Flutter tarball in the release (Flutter packages are staged from the native archives above, not from a dedicated release asset) and **no** `.dmg` asset in the baseline set (the macOS rcli tarball is what ships; see the notarization section for when a `.dmg` does get produced). If a `qhexrt`-named asset appears anywhere in this list, stop — that would mean `release.yml`'s own manifest assertion should have hard-failed and something is very wrong; do not publish anything from that release.\n\n## 0. MANDATORY GATE — verify the ENTIRE release before pushing anything to any registry\n\n**Run this once, in full, before the first registry push. Do not publish a single package until it passes.** Per-package spot-checks are not a substitute: they are how a bad artifact reaches a registry you cannot un-publish from. Proven end to end on v0.20.25 (2026-08-22), then independently audited and corrected: **34 payloads, 6619 files across 3 nesting levels, 282 binaries, 0 placeholder hits, 38/38 commons binaries carrying the real staging host**. The audit found 4 methodology gaps in the first pass (shallow nesting, missing `MTLB` magic, `.lib` excluded from the positive check, and a set-intersection that could pass vacuously) — every one is called out inline below, because each produced a *confident but under-covered* result.\n\n```bash\nVERSION=0.20.25                      # no leading v\nREPO=RunanywhereAI/runanywhere-sdks\nW=/tmp/relverify-$VERSION\nrm -rf \"$W\" && mkdir -p \"$W/assets\" \"$W/qhexrt\" \"$W/x\" \"$W/nested\"\n\n# (1) EVERY release asset — no -p filter, get the whole set.\ngh release download \"v$VERSION\" --repo \"$REPO\" -D \"$W/assets\"\n\n# (2) The PRIVATE QHexRT artifact is NOT a release asset — pull it from the run.\n#     Find the run that produced the release, then:\ngh run download <release-run-id> --repo \"$REPO\" --name sdk-electron-qhexrt-private --dir \"$W/qhexrt\"\n```\n\n### (3) Checksums — `cd` into the directory first\n\nSidecars contain a **bare basename**, so `shasum -c` fails with a misleading \"FAILED open or read\" if run from anywhere else. This will make every artifact look broken when nothing is wrong:\n\n```bash\ncd \"$W/assets\"\nok=0; bad=0\nfor s in *.sha256; do\n  if shasum -a 256 -c \"$s\" >/dev/null 2>&1; then ok=$((ok+1)); else echo \"MISMATCH: $s\"; bad=$((bad+1)); fi\ndone\necho \"verified=$ok mismatched=$bad\"      # bad MUST be 0\n(cd \"$W/qhexrt\" && shasum -a 256 -c ./*.sha256)\n```\n\n### (4) Extract everything — INCLUDING nested archives\n\n**The gotcha that matters:** `runanywhere-kotlin-maven-v*.zip` contains `.aar` files, and the `.aar`s are what hold the Android `.so` binaries. A one-level extraction reports \"0 binaries\" for the Maven bundle and every subsequent scan passes **vacuously**.\n\n**Recurse until the tree is dry — TWO levels are required here, not one.** Each `.aar` also contains a `classes.jar`, holding ~2027 `.class` files that are doubly unreachable: inside an unopened jar *and* excluded by the `.class` suffix rule most magic-byte scanners use. Level 2 adds **2033 files** (0 placeholder hits on v0.20.25); level 3 is empty. Loop until a pass finds no new archives rather than hardcoding a depth.\n\n```bash\ncd \"$W/assets\"\nfor f in *; do\n  case \"$f\" in *.sha256|MANIFEST.txt) continue;; esac\n  d=\"$W/x/$f\"; mkdir -p \"$d\"\n  case \"$f\" in\n    *.zip)              unzip -qq -o \"$f\" -d \"$d\" || echo \"EXTRACT FAILED: $f\" ;;\n    *.tar.gz|*.tgz)     tar -xzf \"$f\" -C \"$d\"     || echo \"EXTRACT FAILED: $f\" ;;\n    *) echo \"UNHANDLED TYPE: $f\" ;;\n  esac\ndone\nd=\"$W/x/$(basename \"$W\"/qhexrt/*.tgz)\"; mkdir -p \"$d\"; tar -xzf \"$W\"/qhexrt/*.tgz -C \"$d\"\n\n# nested pass\ncd \"$W/x\"\nfind . -type f \\( -name '*.aar' -o -name '*.jar' -o -name '*.zip' -o -name '*.tgz' -o -name '*.tar.gz' \\) \\\n  | sed 's|^\\./||' | while IFS= read -r a; do\n    dd=\"$W/nested/$(printf '%s' \"$a\" | tr '/' '_')\"; mkdir -p \"$dd\"\n    case \"$a\" in *.aar|*.jar|*.zip) unzip -qq -o \"$a\" -d \"$dd\";; *) tar -xzf \"$a\" -C \"$dd\";; esac\n  done\n```\n\n**Prove extraction actually happened** — an empty dir makes \"0 hits\" meaningless:\n\n```bash\nfor d in \"$W\"/x/*/; do\n  n=$(find \"$d\" -type f | wc -l)\n  [ \"$n\" -lt 2 ] && echo \"SUSPICIOUSLY EMPTY: $d ($n files)\"\ndone\n```\n\n### (5) Placeholder scan — EVERY file, not just binaries\n\nThe staging URL can also live in JS/JSON/TS config, so scan by content across all file types, and additionally scan the raw compressed streams in case a member never got extracted:\n\n```bash\nfind \"$W/x\" \"$W/nested\" -type f | wc -l                                     # total files covered\ngrep -rla \"YOUR_STAGING_BASE_URL\" \"$W/x\" \"$W/nested\" | tee /tmp/ph.txt | wc -l   # MUST be 0\nfor f in \"$W\"/assets/*.tgz \"$W\"/assets/*.tar.gz \"$W\"/qhexrt/*.tgz; do\n  gzip -dc \"$f\" 2>/dev/null | grep -qa \"YOUR_STAGING_BASE_URL\" && echo \"RAW HIT: $f\"\ndone\n```\n\nFind binaries by **magic bytes, not extension** (an unsuffixed framework binary like `RACommons.framework/RACommons` has no extension and a name-based `find` misses it). Signatures to cover:\n\n| Magic | Kind | v0.20.25 count |\n|---|---|---|\n| `7f454c46` | ELF | 177 |\n| `!<arch>` (`213c617263683e0a`) | static `.a` **and `.lib`** | 41 |\n| `MZ` | PE | 29 |\n| `cffaedfe` / `cefaedfe` | Mach-O | 19 |\n| `00 61 73 6d` | wasm | 10 |\n| **`4d544c42` (`MTLB`)** | **Metal library** — easy to miss | 5 |\n| `cafebabe` / `bebafeca` | fat Mach-O (exclude `.jar`/`.class`) | 1 |\n\n**Total: 282 binaries.** An earlier pass reported 277 because `MTLB` was absent from the signature list — `.metallib` files were invisible to the binary scan (they were still covered by the all-file `grep` in step 5, so the conclusion held, but the count was wrong). If your total comes in materially under ~282 for a comparable release, your detection is leaking.\n\n### (6) POSITIVE staging-URL check — absence of the placeholder is NOT sufficient\n\nA build with an **empty** URL baked in passes a placeholder-only check. `check_plugin_natives.py` only tests absence, so add the positive assertion yourself.\n\nTwo traps that produced false confidence on the v0.20.25 pass, both worth avoiding:\n\n- **Include `.lib`.** A `MUST_HAVE` regex of `rac_commons\\.(dylib|so|dll|a)$` silently skips\n  `RACommons-windows-x64/x64/rac_commons.lib` — an **89 MB** binary that then gets no positive\n  check at all. Match `(dylib|so|dll|a|lib)` plus `\\.wasm$`. Correct coverage is **38** binaries,\n  not 37.\n- **Assert per binary, not by set-intersection.** Intersecting \"non-public origins across all\n  files\" yields more than one origin here (`hf.co` *and* the staging host), so a binary carrying\n  only `hf.co` would pass **vacuously**. Check each binary individually for the staging host.\n\n```bash\n# v0.20.25 baseline: 38 commons/wasm/lib binaries; 0 missing the staging host.\n```\n\nNever print the URL — it is a repo secret. Report a truncated SHA-256 fingerprint, or just assert presence/absence counts.\n\n### (7) Routability gate over the whole tree\n\n```bash\npython3 scripts/validation/gates/check_plugin_natives.py \"$W/x\" \"$W/nested\" > /tmp/gate.txt 2>&1\ngrep -cE '^  ok ' /tmp/gate.txt; grep -E '^  FAIL' /tmp/gate.txt\n```\n\nv0.20.25 baseline: **77 ok, 1 FAIL**. The single expected FAIL is\n`RACommons-linux-x86_64/lib/librunanywhere_sherpa.so` — see the known-exception note below. **Any other FAIL blocks the publish.**\n\n### (8) Known exception — Linux sherpa is a non-routable shell (PRE-EXISTING, not a regression)\n\n`RACommons-linux-x86_64-v*.tar.gz` ships **no** `libsherpa-onnx-c-api.so` at all and its `librac_backend_sherpa.so` is a ~156 KB shell (~3 sherpa strings, vs ~31 MB / ~192 for a healthy one on Electron macOS) — the engine is not linked in, so the carrier cannot reference `g_sherpa_{stt,tts,vad}_ops` and **STT/TTS/VAD cannot work from that bundle**. Android's carriers pass on all 3 ABIs, so this is Linux-only.\n\n**Compare the CARRIER, not the backend.** The gate FAILs on `librunanywhere_sherpa.so` (the carrier); citing `librac_backend_sherpa.so` as your evidence is comparing a *different file* and is not proof. The carrier is byte-identical across releases, which is the real proof:\n\n```bash\ngh release download \"v<PREV>\" --repo \"$REPO\" -p 'RACommons-linux-x86_64-v<PREV>.tar.gz' -D /tmp/prev\ntar -xzf /tmp/prev/*.tar.gz -C /tmp/prev\nshasum -a 256 /tmp/prev/lib/librunanywhere_sherpa.so \"$W\"/x/RACommons-linux-*/lib/librunanywhere_sherpa.so\n# v0.20.24 and v0.20.25 both: size=15712  sha256=28ef16015b61da86...\n\n# strongest check — run the gate on the PREVIOUS bundle and confirm the identical FAIL:\npython3 scripts/validation/gates/check_plugin_natives.py /tmp/prev\n```\n\nIf it ever regresses *further* (e.g. Android or macOS sherpa also goes hollow), stop and treat it as a blocking defect.\n\n### (9) Account for every payload explicitly\n\nPrint a per-artifact table (checksum verdict, binary count, placeholder hits) and confirm the row count equals the payload count. Artifacts that legitimately contain **no** native binaries, and why — do not let these pass silently unexamined:\n\n| Artifact | Why it has no binaries |\n|---|---|\n| `RunAnywhereMLXResources-ios-*.zip` | 25 third-party license/notice text files |\n| `runanywhere-kotlin-maven-v*.zip` | binaries live in the **nested** `.aar`s (step 4) |\n| `runanywhere-proto-ts-*.tgz` | pure TS/JS (69 `.ts` + 69 `.js`) |\n\nEverything else must have a non-zero, plausible binary count. A platform suddenly shipping fewer binaries than the previous release is a regression signal worth chasing before publishing.\n\n### (10) Blind spot to be aware of — Windows x64 has NO routability coverage\n\n`RACommons-windows-x64-v*.zip` ships **only** a static `rac_commons.lib` (~89 MB) plus 152 `.h` / 11 `.hpp` headers — **zero backend DLLs**, where Linux/Android/Web each ship 4–5 backend carriers. That is consistent with a static-embedding bundle (consumers link the engines in themselves), but the consequence matters: **no gate can detect a missing or hollow Windows backend**, because there is no carrier to inspect. `check_plugin_natives.py` therefore reports nothing at all for this artifact rather than reporting health.\n\nWindows x64 is also *advisory* in `release.yml` (its absence does not fail the release). So a Windows regression can pass every automated check. If Windows correctness matters for a given release, verify it by actually running something on the real Windows box (see §Electron for how to reach it) — do not infer it from a green pipeline. Worth a deliberate confirmation with the repo owner that the DLL-free shape is intentional.\n\n---\n\n## General verification discipline (every artifact, every registry)\n\nApply all three checks below to **every** file you're about to publish, before AND after the publish call. Never trust a copy cached locally earlier in the session — re-download.\n\n**(a) Download + verify the sha256 sidecar yourself:**\n\n```bash\ngh release download v$VERSION -p '<file>' -p '<file>.sha256' -D /tmp/sdk-publish-$VERSION\ncd /tmp/sdk-publish-$VERSION && shasum -a 256 -c '<file>.sha256'\n```\n\n**(b) Inspect the actual file listing inside every tarball/AAR/zip** — never assume from the filename alone:\n\n```bash\nnpm pack <tgz-or-package-dir> --dry-run     # npm packages: prints the exact file list it would publish\nunzip -l  <file.zip>                        # AARs, Maven repo zip, native archives\ntar -tzf  <file.tar.gz>                     # rcli, RACommons tarballs\n```\n\nSanity-check what comes back:\n- No suspiciously-small or empty binaries where a real native library (`.so`, `.dylib`, `.a`, `.dll`) should be — a stub or a zero-byte placeholder means the build silently failed upstream.\n- No leftover `workspace:*` or `file:` dependency specs in any `package.json` inside the tarball — those only resolve inside this monorepo and break for a real external installer.\n- Version strings inside every manifest (`package.json` `version`, AAR `pom.xml` `<version>`, `pubspec.yaml` `version:`) **exactly** match `$VERSION`.\n- **Hard rule, no exceptions:** grep the full file listing for `qhexrt` (any case) and for QNN binary names (`libQnnHtp`, `QnnHtp*.dll`, `rac_commons.dll` bundled with QHexRT, `runanywhere_qhexrt`). If any hit appears in a package meant to be public, stop and do not publish that artifact.\n\n**(c) Confirm baked-in runtime config** (e.g. a staging-vs-prod backend base URL compiled into `rac_commons` at build time) points at the real intended endpoint. This is designed to fail closed if misconfigured — a build that completed and produces working requests against prod is itself decent evidence — but if you have any doubt, `strings <binary> | grep -i \"api\\.\\|staging\\.\"` and eyeball it.\n\n## 1. npm — 8 packages, order matters\n\nPackages: 4 Web (`@runanywhere/proto-ts`, `@runanywhere/web`, `@runanywhere/web-llamacpp`, `@runanywhere/web-onnx`) + 4 React Native (`@runanywhere/core`, `@runanywhere/llamacpp`, `@runanywhere/mlx`, `@runanywhere/onnx`). Never `@runanywhere/qhexrt` (RN) — it exists in `bindings/react-native/packages/qhexrt/` but is excluded from the public npm train by the same policy as Maven/pub.dev QHexRT.\n\n**Publish `@runanywhere/proto-ts` FIRST.** `@runanywhere/web-onnx` deliberately does not bundle its own `proto-ts` copy and needs it resolvable from the live registry for real consumers; publish `proto-ts` before it or a fresh `npm install @runanywhere/web-onnx` fails to resolve.\n\n```bash\ngh release download v$VERSION -p 'runanywhere-proto-ts-*.tgz*' -p 'runanywhere-web-*.tgz*' \\\n  -p 'runanywhere-core-*.tgz*' -p 'runanywhere-llamacpp-*.tgz*' -p 'runanywhere-mlx-*.tgz*' -p 'runanywhere-onnx-*.tgz*' \\\n  -D /tmp/sdk-publish-$VERSION\ncd /tmp/sdk-publish-$VERSION\nfor f in *.sha256; do shasum -a 256 -c \"$f\"; done\n\nnpm publish runanywhere-proto-ts-$VERSION.tgz --access public   # FIRST, always\nnpm publish runanywhere-web-$VERSION.tgz --access public\nnpm publish runanywhere-web-llamacpp-$VERSION.tgz --access public\nnpm publish runanywhere-web-onnx-$VERSION.tgz --access public\nnpm publish runanywhere-core-$VERSION.tgz --access public\nnpm publish runanywhere-llamacpp-$VERSION.tgz --access public\nnpm publish runanywhere-mlx-$VERSION.tgz --access public\nnpm publish runanywhere-onnx-$VERSION.tgz --access public\n```\n\n`npm whoami` should already show a logged-in account; if it errors, stop and tell the human rather than trying to log in non-interactively.\n\n**GOTCHA — large tarballs can silently die mid-upload.** `@runanywhere/mlx` (~50MB) and `@runanywhere/web-onnx` (unpacks to 300+MB) can exceed a short default shell/tool timeout mid-upload and get killed with no clean error. Use an explicit long timeout (5 minutes / 300000ms) on each publish call.\n\n**GOTCHA — verify via the registry API, not the npm CLI.** `npm view` can lag the CLI's own cache by several seconds right after a fresh publish and falsely report \"not found\". After **every** publish:\n\n```bash\ncurl -s \"https://registry.npmjs.org/@runanywhere/<pkg>\" | python3 -c \"import json,sys;print(json.load(sys.stdin)['dist-tags'])\"\n```\nConfirm `latest` equals `$VERSION`.\n\n## 2. Maven Central — 3 AARs only\n\n`io.github.sanchitmonga22:{runanywhere-sdk,runanywhere-llamacpp,runanywhere-onnx}`. **Never `runanywhere-qhexrt-android`** — it was published through 0.20.19 under an earlier, since-reversed policy and is now permanently excluded; `release.yml`'s manifest assertion hard-fails if it reappears in the public asset set.\n\nFull step-by-step lives in `bindings/kotlin/docs/KOTLIN_MAVEN_CENTRAL_PUBLISHING.md`, section **\"Option A: Publish with released native archives\"** (line ~108 as of this writing — the doc has been corrected mid-release before, so re-read it fresh rather than trusting memory of its contents). Key points, condensed:\n\n1. Download + verify the 3 `RACommons-android-{arm64-v8a,armeabi-v7a,x86_64}-v$VERSION.zip` from the release (sha256 sidecars, per the general discipline above).\n2. Stage: `bash bindings/kotlin/scripts/package-sdk.sh --mode local --natives-from <dir-with-the-3-zips>`. This also produces a local Maven-repo zip — diff its size against `runanywhere-kotlin-maven-v$VERSION.zip` already attached to the GitHub Release as a cheap reproducibility check.\n3. Before uploading, confirm the OSSRH compatibility service has no dangling open repository for this namespace:\n   ```bash\n   CENTRAL_BEARER=\"$(printf '%s:%s' \"$MAVEN_CENTRAL_USERNAME\" \"$MAVEN_CENTRAL_PASSWORD\" | base64 | tr -d '\\r\\n')\"\n   curl -s -H \"Authorization: Bearer $CENTRAL_BEARER\" \\\n     'https://ossrh-staging-api.central.sonatype.com/manual/search/repositories?ip=client&profile_id=io.github.sanchitmonga22'\n   ```\n   Resolve any stale repository per the doc's troubleshooting table before continuing.\n4. Source `MAVEN_CENTRAL_USERNAME`/`MAVEN_CENTRAL_PASSWORD` from `~/.gradle/gradle.properties` (`mavenCentral.username`/`mavenCentral.password` — confirmed present on this machine; never print the values) and run, from `bindings/kotlin/`:\n   ```bash\n   ./gradlew clean \\\n     :publishReleasePublicationToMavenCentralRepository \\\n     :modules:runanywhere-core-llamacpp:publishReleasePublicationToMavenCentralRepository \\\n     :modules:runanywhere-core-onnx:publishReleasePublicationToMavenCentralRepository \\\n     -Prunanywhere.useLocalNatives=true -x buildLocalJniLibs --no-daemon\n   ```\n   GPG signing (`signing.gnupg.keyName`/`signing.gnupg.passphrase` in the same properties file, key id `CC377A9928C7BB18` — confirmed importable via `gpg --list-secret-keys --keyid-format LONG` on this machine) is read from the same properties file automatically; you do not pass it separately.\n5. Transfer to Central Portal:\n   ```bash\n   curl --fail --request POST -H \"Authorization: Bearer $CENTRAL_BEARER\" \\\n     'https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/io.github.sanchitmonga22?publishing_type=automatic'\n   ```\n   Then find the resulting `portal_deployment_id` — expect **exactly one** fresh repository; if the count isn't 1, stop and investigate rather than guessing which one is yours (see the doc for the exact `jq` filter).\n6. Poll `https://central.sonatype.com/api/v1/publisher/status?id=<id>` (POST, same bearer) until `deploymentState` is `PUBLISHED`. Automatic deployments normally go `PENDING -> VALIDATING -> PUBLISHING -> PUBLISHED` within a few minutes. **If it lands on `VALIDATED` instead of proceeding, that means Sonatype is waiting for a manual click in the Central Portal UI** — stop and tell the human; do not loop forever assuming it resolves itself. On `FAILED`, print `.errors` and stop.\n7. Verify propagation (allow 10-30 minutes for CDN propagation even after `PUBLISHED`):\n   ```bash\n   for a in runanywhere-sdk runanywhere-llamacpp runanywhere-onnx; do\n     echo \"$a: $(curl -s -o /dev/null -w '%{http_code}' \"https://repo1.maven.org/maven2/io/github/sanchitmonga22/$a/$VERSION/$a-$VERSION.pom\")\"\n   done\n   ```\n   Expect `200` for all three.\n\n## 3. pub.dev (Flutter)\n\n**Verify the current package set fresh — do not trust a hardcoded count.** As of this writing the real, non-QHexRT Flutter package set is **4**, not 3: `runanywhere`, `runanywhere_llamacpp`, `runanywhere_onnx`, and `runanywhere_mlx` (mlx was added after some earlier documentation/memory of \"3 packages\" was written — that assumption is stale; `runanywhere_mlx` is a real, already-published-on-pub.dev package, not new/WIP). Enumerate it yourself instead of trusting this list verbatim next time:\n```bash\nfind bindings/flutter/packages -maxdepth 1 -type d -not -iname '*qhexrt*' -not -path '*/packages'\n```\n`runanywhere_qhexrt` exists in that directory too — never publish it (same exclusion policy as Maven).\n\nStage the released iOS xcframeworks + Android `.so` into each package with the Flutter equivalent of `package-sdk.sh` (melos-based monorepo, confirmed at `bindings/flutter/scripts/package-sdk.sh`):\n\n```bash\nbash bindings/flutter/scripts/package-sdk.sh --mode local --natives-from <dir-with-xcframeworks-and-android-.so>\n```\n\nThis script already runs `flutter pub publish --dry-run` internally per package as part of its own validation — but run it again yourself, per package, from each package directory, and **read the output for real, not just the exit code**:\n\n```bash\ncd bindings/flutter/packages/runanywhere && flutter pub publish --dry-run\ncd ../runanywhere_llamacpp && flutter pub publish --dry-run\ncd ../runanywhere_onnx && flutter pub publish --dry-run\ncd ../runanywhere_mlx && flutter pub publish --dry-run\n```\n\nExpect zero warnings except a benign version-gap hint. Only then run the real publish, same order (core first, since the others depend on it):\n\n```bash\ncd bindings/flutter/packages/runanywhere && flutter pub publish\ncd ../runanywhere_llamacpp && flutter pub publish\ncd ../runanywhere_onnx && flutter pub publish\ncd ../runanywhere_mlx && flutter pub publish\n```\n\n**Credential note:** pub.dev OAuth is normally cached at `~/Library/Application Support/dart/pub-credentials.json` on macOS (confirmed present on this machine). If it's missing, expired, or `flutter pub publish` prompts for an interactive browser login, **STOP and tell the human** — you cannot complete a fresh Google OAuth flow non-interactively, and forcing/guessing here is not appropriate.\n\nVerify afterward against the live API (not a cached page):\n```bash\nfor p in runanywhere runanywhere_llamacpp runanywhere_onnx runanywhere_mlx; do\n  echo \"$p: $(curl -s \"https://pub.dev/api/packages/$p\" | python3 -c \"import json,sys;print(json.load(sys.stdin)['latest']['version'])\")\"\ndone\n```\n\n## 4. Swift SPM distribution repo (`RunanywhereAI/runanywhere-swift`)\n\nA separate, **generated-only** repo mirroring `bindings/swift` (Package.swift + Sources/ + LICENSE + README) so SwiftPM consumers using `from: \"<version>\"` don't clone the ~340MB monorepo.\n\n**GOTCHA:** this machine may have local directories literally named `runanywhere-swift` that are just checkouts of the **monorepo's** remote, not the distribution repo. Verify before trusting any existing local directory by name:\n```bash\ngit -C <candidate-dir> remote get-url origin\n```\nMust show `RunanywhereAI/runanywhere-swift`, not `runanywhere-sdks`. Always clone fresh instead of guessing:\n\n```bash\ngit clone https://github.com/RunanywhereAI/runanywhere-swift.git /tmp/ra-swift\n\nbindings/swift/scripts/sync-dist-repo.sh \\\n  --zips <exact-verified-Apple-archive-dir-from-the-release> --tag /tmp/ra-swift\n\nRUNANYWHERE_SWIFT_DIST_REPO=/tmp/ra-swift \\\n  bash scripts/validation/gates/check_swift_dist_repo_sync.sh\n\ngit -C /tmp/ra-swift push origin main --follow-tags\n```\n\nThe `--zips` dir must contain the verified (sha256-checked) release archives — `RACommons-ios-v$VERSION.zip`, `RABackend{LLAMACPP,MLX,NeuRT,ONNX,Sherpa}-ios-v$VERSION.zip`, `RunAnywhereMLX{Metal,Resources,Runtime}-ios-v$VERSION.zip` — downloaded and checked exactly per the general verification discipline, not re-used from an earlier, untrusted local copy.\n\n**The tag on `runanywhere-swift` is bare semver, no `v` prefix** (SwiftPM's `from:` needs that) — do not tag it `v$VERSION` by mistake; `sync-dist-repo.sh --tag` handles this correctly, but double check the pushed tag with `git -C /tmp/ra-swift tag --points-at HEAD` before walking away.\n\nThis is enforced, not just documented: once `v$VERSION` is tagged on `runanywhere-sdks`, `check_swift_dist_repo_sync.sh` fails every future PR on the monorepo until `runanywhere-swift` carries the matching bare-semver tag — so don't let this lag behind the other registries.\n\n**GATE GAP — `check_swift_dist_repo_sync.sh` does NOT catch dependency drift, only checksum/version-string drift.** `runanywhere-swift`'s `Package.swift` is manually maintained end to end — `sync-dist-repo.sh` only ever touches `sdkVersion` and the binaryTarget checksums, never the `dependencies:` block (mlx-swift, mlx-swift-lm, mlx-audio-swift, swift-crypto, etc.). If root `runanywhere-sdks/Package.swift` bumps one of those (e.g. a new fork tag), a human has to remember to mirror it into the dist repo by hand, and nothing fails CI if they forget. This exact drift shipped once (2026-08-17): the dist repo pointed `mlx-swift-lm` at unforked `ml-explore/mlx-swift-lm` instead of the `RunanywhereAI` fork, silently breaking every iOS/macOS MLX consumer of that tag until caught by an actual build failure ~40 minutes later. Before trusting a freshly-cut dist-repo tag: diff every `.package(url:` line between root `Package.swift` and the dist repo's `Package.swift` (`grep -n '\\.package(url:' Package.swift` on both sides), and actually build a real product that exercises the dependency you're worried about (e.g. `cd /tmp/ra-swift && swift build --product RunAnywhereMLX`) — don't rely on the sync gate alone. If you catch drift on a tag that's only minutes old with no evidence of external consumption, fixing it in place and force-moving the tag (`git tag -f $VERSION && git push origin $VERSION --force`) is the correct call, not a shortcut — a known-broken immutable tag helps no one.\n\n**If you force-move a tag, SwiftPM/Xcode consumers cache the OLD commit in several places — clearing just `Package.resolved` is not enough.** Verifying your own fix (or having a downstream consumer verify it) can silently keep failing even after the fix is correctly pushed, because: (1) any consumer repo's own tracked `*.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved` pins the exact old resolution and `xcodebuild -resolvePackageDependencies` respects it rather than re-resolving; (2) `~/Library/Caches/org.swift.swiftpm/repositories/<pkg>-<hash>/` is a global shared clone cache keyed by repo, oblivious to a moved tag; (3) `~/Library/org.swift.swiftpm/security/fingerprints/<pkg>-<hash>.json` records the fingerprint SwiftPM saw for that tag and can silently keep using it after a mismatch instead of failing loud. To actually re-resolve after moving a tag: delete the consumer's tracked `Package.resolved`, `rm -rf .build .swiftpm` and any `~/Library/Developer/Xcode/DerivedData/<Scheme>-*`, AND clear the matching entries under both global `org.swift.swiftpm` cache directories for every affected package identity — then resolve fresh.\n\n## 5. macOS rcli notarization\n\nCI (`native_rcli_macos` in `release.yml`) usually already produces a notarized, stapled artifact when Developer ID + notary secrets are configured in repo secrets. **Check the release assets first** before assuming you need to notarize anything yourself locally:\n\n```bash\ngh release view v$VERSION --json assets --jq '.assets[].name' | grep -iE 'rcli-macos|\\.dmg'\n```\nBaseline expectation is `rcli-macos-arm64-v$VERSION.tar.gz` (+ `.sha256`); a `.dmg` only appears when `RCLI_MACOS_FULL_RELEASE=1` was set for that build (see below). If a plain tarball is already present and its contents pass the general verification discipline, there is usually nothing further to do here.\n\n**If you do need to notarize locally**, everything lives in `rcli/scripts/package-rcli.sh` (read its header comment — it documents its own contract in detail):\n\n- One-time setup, check before redoing: `xcrun notarytool history --keychain-profile runanywhere-notary` (confirmed configured on this machine — a fresh submission history returns without error). If missing: `xcrun notarytool store-credentials \"runanywhere-notary\" --apple-id <email> --team-id <team> --password <app-specific-password>`.\n- Codesign identity: `security find-identity -v -p codesigning | grep \"Developer ID Application\"` — confirmed present on this machine as `\"Developer ID Application: RunAnywhere, Inc (<TEAM_ID>)\"`. Grep for it fresh rather than hardcoding the team-id suffix, it can rotate.\n- Invocation contract (env vars, from the script header): `RCLI_CODESIGN_IDENTITY`, `RCLI_MACOS_NOTARIZE=1`, `RCLI_MACOS_FULL_RELEASE=1`, `RCLI_MACOS_SWIFT_BIN_DIR=<output of build-mlx-cli.sh>`. `RCLI_MACOS_FULL_RELEASE=1` is what additionally stages `mlx.metallib` and SwiftPM resource bundles and is what causes a notarized, stapled `.dmg` to be emitted alongside the Homebrew-style tarball. `RCLI_CODESIGN_KEYCHAIN` / `RCLI_NOTARYTOOL_KEYCHAIN` matter only if the identity/profile live in a non-default keychain.\n- Call shape: `rcli/scripts/package-rcli.sh <build-dir> macos-arm64` with the env vars above exported.\n\n## 6. Electron — build AND packaging are now in `release.yml`; publish is still manual\n\n**Reversing the earlier \"deliberately separate\" note**: as of `fix/electron-cicd-staging-url-npu-ane` (2026-08-21), the repo owner asked for Electron to produce ready-to-publish artifacts through the real release train like every other SDK, so `release.yml` now has `native_electron` (invokes `.github/workflows/electron-native-package.yml` as a reusable `workflow_call`) and `sdk_electron` (downloads its `electron-packaged-tarballs` artifact, runs `scripts/release/prepublish_check.py` on every tarball, checksums them, and splits public vs. QHexRT-private). `publish` hard-gates on `sdk_electron` succeeding. `@runanywhere/electron`, `-llamacpp`, `-onnx`, `-sherpa`, `-neurt` land as real, `.sha256`-sidecarred GitHub Release assets, same as the Web/RN npm tarballs. `@runanywhere/electron-qhexrt` still never becomes a public Release asset (policy below) — it's produced and packaged every release, but only reachable as the `sdk-electron-qhexrt-private` workflow artifact on that run, for a human to `npm publish` by hand. Electron is **not** WIP on a branch anymore: the old root `AGENTS.md` \"Active issues\" section describing `smonga/electron_upgrade` as in-progress is gone entirely as of this checkout — `bindings/electron/` is a fully merged, real SDK on `main`. Re-check that section is still absent before trusting this note.\n\n`.github/workflows/electron-native-package.yml` itself is unchanged in what it builds: every backend for real (llamacpp/onnx/sherpa/neurt on their real platforms, qhexrt on a self-hosted Windows ARM64 runner) with the real `STAGING_BASE_URL` baked in, gated by `check_plugin_natives.py` (which fails closed on the staging-URL placeholder — the exact bug that shipped through `0.20.24`). What's new is that it's also invocable via `workflow_call`, and that `native_win_x64_and_package` now tolerates a failed/skipped QHexRT lane (self-hosted, single machine) rather than losing the other five real packages to one runner's outage. **`release.yml` itself still does not `npm publish` anything** — it only produces artifacts (Release assets for the public five, a workflow artifact for QHexRT) — same \"build automated, publish manual\" shape as every other platform. See `thoughts/shared/plans/electron_cicd_staging_url_and_npu_ane_pipeline.md` for the full history.\n\n**PROVEN END TO END on v0.20.25 (2026-08-22)** — the first release where this actually ran. Final state: 65 release assets, all 5 public Electron tarballs present with `.sha256` sidecars, zero `qhexrt`-named assets. Verified by downloading the *published* tarballs and running the repo's own gate against them:\n\n```bash\ngh release download v<version> --repo RunanywhereAI/runanywhere-sdks -p 'runanywhere-electron*.tgz' -D /tmp/verify\npython3 scripts/validation/gates/check_plugin_natives.py --expect-version <version> \\\n    $(for t in /tmp/verify/*.tgz; do printf -- '--tarball %s ' \"$t\"; done)\n# => All 20 check(s) passed: plugin natives are routable and no staging-URL placeholder was found.\n```\n\nThat single command asserts all three things you actually care about, per binary: the real `STAGING_BASE_URL` is baked in (no `YOUR_STAGING_BASE_URL`), `rac_commons` embeds the right version, and every backend is genuinely **routable** (`g_llamacpp_ops`, `g_sherpa_stt_ops`, `g_neurt_llm_ops`, `g_onnx_embeddings_ops`, …) rather than a same-sized shell. **Run it before every Electron `npm publish`** — it is the cheapest defense against exactly the class of defect that shipped `electron-sherpa` 0.20.17's NULL-vtable carrier. Expected shipped layout: entry package = `darwin-arm64` + `win32-x64` + `win32-arm64`; llamacpp/onnx/sherpa = `darwin-arm64` + `win32-x64`; neurt = `darwin-arm64` only; qhexrt = `win32-arm64` only.\n\nWhere to get the QHexRT tarball for its manual publish (it is deliberately NOT a release asset):\n\n```bash\ngh run download <release-run-id> --repo RunanywhereAI/runanywhere-sdks \\\n    --name sdk-electron-qhexrt-private --dir /tmp/qhexrt\n```\n\nIf asked to actually publish an Electron package (still a real, separate, manual step — this skill's general discipline in §0/§General verification still applies in full):\n- Involve the Windows box for real Windows-native verification before publishing anything Windows-targeted. The team keeps a **Windows ARM64 machine reachable over SSH**; its hostname, user, key, and mesh-VPN node name are operator-specific and deliberately not recorded in this repo — read the Windows/ARM64 host entry out of the operator's `~/.ssh/config` and use that alias. If there is no such entry, the box is not configured for you: say so and ask, rather than guessing at a hostname. Check reachability **first** by asking the mesh-VPN client whether the node is up (e.g. `tailscale status`, matching on the host entry's `HostName`) — if it reports `offline, last seen ...`, the box is asleep, which is not a configuration problem: ask the human to wake it, do not debug it as broken. Only then `ssh -o ConnectTimeout=8 \"$WIN_BOX\" \"whoami\"`. That same machine is **also** registered as a GitHub Actions self-hosted runner (labels `[self-hosted, Windows, ARM64, qhexrt]`) running as a Windows service — a queued `native_win_arm64_qhexrt` job on it means the machine is busy; check `gh api repos/RunanywhereAI/runanywhere-sdks/actions/runners` for the matching runner's `busy`/`status` before assuming it's free for manual SSH work, and vice versa.\n- **`@runanywhere/electron-qhexrt` being public on npm is a CONFIRMED, settled policy decision** (repo owner, 2026-08-20) — it contains real Hexagon NPU binaries (`libQnnHtpV81Skel.so`, `QnnHtp*.dll`, `rac_commons.dll`, `runanywhere_qhexrt.dll`) on purpose. The QHexRT-stays-private policy enforced everywhere else in this skill is specifically about the **GitHub-Release-side** asset set (Android etc.), a different channel — not a contradiction. Stop treating this as an unresolved item to flag; only re-raise it if you find evidence the policy changed.\n\n## 7. Final sanity pass\n\nAfter every registry is done, re-view each package's public listing/API and compare against the previous version — a big unexplained drop in file count or package size is a red flag to investigate before calling the release fully published:\n\n```bash\n# npm\nfor p in proto-ts web web-llamacpp web-onnx core llamacpp mlx onnx; do\n  npm pack @runanywhere/$p@$VERSION --dry-run 2>&1 | tail -5\ndone\n\n# Maven — check module metadata / POM sizes are non-trivial, not near-empty\nfor a in runanywhere-sdk runanywhere-llamacpp runanywhere-onnx; do\n  curl -s -o /dev/null -w \"%{size_download} bytes  $a\\n\" \\\n    \"https://repo1.maven.org/maven2/io/github/sanchitmonga22/$a/$VERSION/$a-$VERSION.aar\"\ndone\n\n# pub.dev\nfor p in runanywhere runanywhere_llamacpp runanywhere_onnx runanywhere_mlx; do\n  curl -s \"https://pub.dev/api/packages/$p\" | python3 -c \"import json,sys;d=json.load(sys.stdin);print('$p', d['latest']['version'])\"\ndone\n```\n\nOnly after this pass — every registry shows `$VERSION`, every artifact's contents check out, and nothing regressed in size/file-count versus the previous release — is the release fully published. Do not skip this step just because each individual publish call reported success; a successful upload is not the same claim as \"the right bytes are now live.\"\n","core/AGENTS.md":"# core - AI Agent Protocol & Instructions\n\nPath: `core/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/core/AGENTS.md)","core/CLAUDE.md":"# core - Claude Agent Guidelines & System Prompt\n\nPath: `core/CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/core/CLAUDE.md)","engines/AGENTS.md":"# engines - AI Agent Protocol & Instructions\n\nPath: `engines/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/engines/AGENTS.md)","engines/CLAUDE.md":"# engines - Claude Agent Guidelines & System Prompt\n\nPath: `engines/CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/engines/CLAUDE.md)","idl/AGENTS.md":"# idl - AI Agent Protocol & Instructions\n\nPath: `idl/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/idl/AGENTS.md)","idl/CLAUDE.md":"# idl - Claude Agent Guidelines & System Prompt\n\nPath: `idl/CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/idl/CLAUDE.md)","runtimes/AGENTS.md":"# runtimes - AI Agent Protocol & Instructions\n\nPath: `runtimes/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/runtimes/AGENTS.md)","runtimes/CLAUDE.md":"# runtimes - Claude Agent Guidelines & System Prompt\n\nPath: `runtimes/CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/runtimes/CLAUDE.md)","scripts/AGENTS.md":"# scripts - AI Agent Protocol & Instructions\n\nPath: `scripts/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/scripts/AGENTS.md)","scripts/CLAUDE.md":"# scripts - Claude Agent Guidelines & System Prompt\n\nPath: `scripts/CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/scripts/CLAUDE.md)",".claude/commands/address_PR_comments.md":"# Command: /address_pr_comments\n\nPath: `.claude/commands/address_PR_comments.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/.claude/commands/address_PR_comments.md)",".claude/commands/copy_over_logic.md":"# Command: /copy_over_logic\n\nPath: `.claude/commands/copy_over_logic.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/.claude/commands/copy_over_logic.md)",".claude/commands/run_and_fix_lints.md":"# Command: /run_and_fix_lints\n\nPath: `.claude/commands/run_and_fix_lints.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/.claude/commands/run_and_fix_lints.md)","bindings/electron/packages/llamacpp/README.md":"# llamacpp Documentation\n\nPath: `bindings/electron/packages/llamacpp/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/llamacpp/README.md)","bindings/electron/packages/llamacpp/package-lock.json":"# Llamacpp Skill\n\nPath: `bindings/electron/packages/llamacpp/package-lock.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/llamacpp/package-lock.json)","bindings/electron/packages/llamacpp/package.json":"# Llamacpp Skill\n\nPath: `bindings/electron/packages/llamacpp/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/llamacpp/package.json)","bindings/electron/packages/llamacpp/tsconfig.json":"# Llamacpp Skill\n\nPath: `bindings/electron/packages/llamacpp/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/llamacpp/tsconfig.json)","bindings/electron/packages/neurt/README.md":"# neurt Documentation\n\nPath: `bindings/electron/packages/neurt/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/neurt/README.md)","bindings/electron/packages/neurt/package-lock.json":"# Neurt Skill\n\nPath: `bindings/electron/packages/neurt/package-lock.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/neurt/package-lock.json)","bindings/electron/packages/neurt/package.json":"# Neurt Skill\n\nPath: `bindings/electron/packages/neurt/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/neurt/package.json)","bindings/electron/packages/neurt/tsconfig.json":"# Neurt Skill\n\nPath: `bindings/electron/packages/neurt/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/neurt/tsconfig.json)","bindings/electron/packages/onnx/README.md":"# onnx Documentation\n\nPath: `bindings/electron/packages/onnx/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/onnx/README.md)","bindings/electron/packages/onnx/package-lock.json":"# Onnx Skill\n\nPath: `bindings/electron/packages/onnx/package-lock.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/onnx/package-lock.json)","bindings/electron/packages/onnx/package.json":"# Onnx Skill\n\nPath: `bindings/electron/packages/onnx/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/onnx/package.json)","bindings/electron/packages/onnx/tsconfig.json":"# Onnx Skill\n\nPath: `bindings/electron/packages/onnx/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/onnx/tsconfig.json)","bindings/electron/packages/qhexrt/README.md":"# qhexrt Documentation\n\nPath: `bindings/electron/packages/qhexrt/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/qhexrt/README.md)","bindings/electron/packages/qhexrt/package-lock.json":"# Qhexrt Skill\n\nPath: `bindings/electron/packages/qhexrt/package-lock.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/qhexrt/package-lock.json)","bindings/electron/packages/qhexrt/package.json":"# Qhexrt Skill\n\nPath: `bindings/electron/packages/qhexrt/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/qhexrt/package.json)","bindings/electron/packages/qhexrt/tsconfig.json":"# Qhexrt Skill\n\nPath: `bindings/electron/packages/qhexrt/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/qhexrt/tsconfig.json)","bindings/electron/packages/sherpa/README.md":"# sherpa Documentation\n\nPath: `bindings/electron/packages/sherpa/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/sherpa/README.md)","bindings/electron/packages/sherpa/package-lock.json":"# Sherpa Skill\n\nPath: `bindings/electron/packages/sherpa/package-lock.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/sherpa/package-lock.json)","bindings/electron/packages/sherpa/package.json":"# Sherpa Skill\n\nPath: `bindings/electron/packages/sherpa/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/sherpa/package.json)","bindings/electron/packages/sherpa/tsconfig.json":"# Sherpa Skill\n\nPath: `bindings/electron/packages/sherpa/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/sherpa/tsconfig.json)","bindings/flutter/packages/runanywhere/CHANGELOG.md":"# Runanywhere Skill\n\nPath: `bindings/flutter/packages/runanywhere/CHANGELOG.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere/CHANGELOG.md)","bindings/flutter/packages/runanywhere/README.md":"# runanywhere Documentation\n\nPath: `bindings/flutter/packages/runanywhere/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere/README.md)","bindings/flutter/packages/runanywhere/pubspec.yaml":"# Runanywhere Skill\n\nPath: `bindings/flutter/packages/runanywhere/pubspec.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere/pubspec.yaml)","bindings/flutter/packages/runanywhere_llamacpp/CHANGELOG.md":"# Runanywhere_llamacpp Skill\n\nPath: `bindings/flutter/packages/runanywhere_llamacpp/CHANGELOG.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_llamacpp/CHANGELOG.md)","bindings/flutter/packages/runanywhere_llamacpp/README.md":"# runanywhere_llamacpp Documentation\n\nPath: `bindings/flutter/packages/runanywhere_llamacpp/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_llamacpp/README.md)","bindings/flutter/packages/runanywhere_llamacpp/pubspec.yaml":"# Runanywhere_llamacpp Skill\n\nPath: `bindings/flutter/packages/runanywhere_llamacpp/pubspec.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_llamacpp/pubspec.yaml)","bindings/flutter/packages/runanywhere_mlx/CHANGELOG.md":"# Runanywhere_mlx Skill\n\nPath: `bindings/flutter/packages/runanywhere_mlx/CHANGELOG.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_mlx/CHANGELOG.md)","bindings/flutter/packages/runanywhere_mlx/README.md":"# runanywhere_mlx Documentation\n\nPath: `bindings/flutter/packages/runanywhere_mlx/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_mlx/README.md)","bindings/flutter/packages/runanywhere_mlx/pubspec.yaml":"# Runanywhere_mlx Skill\n\nPath: `bindings/flutter/packages/runanywhere_mlx/pubspec.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_mlx/pubspec.yaml)","bindings/flutter/packages/runanywhere_onnx/CHANGELOG.md":"# Runanywhere_onnx Skill\n\nPath: `bindings/flutter/packages/runanywhere_onnx/CHANGELOG.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_onnx/CHANGELOG.md)","bindings/flutter/packages/runanywhere_onnx/README.md":"# runanywhere_onnx Documentation\n\nPath: `bindings/flutter/packages/runanywhere_onnx/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_onnx/README.md)","bindings/flutter/packages/runanywhere_onnx/pubspec.yaml":"# Runanywhere_onnx Skill\n\nPath: `bindings/flutter/packages/runanywhere_onnx/pubspec.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_onnx/pubspec.yaml)","bindings/flutter/packages/runanywhere_qhexrt/CHANGELOG.md":"# Runanywhere_qhexrt Skill\n\nPath: `bindings/flutter/packages/runanywhere_qhexrt/CHANGELOG.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_qhexrt/CHANGELOG.md)","bindings/flutter/packages/runanywhere_qhexrt/README.md":"# runanywhere_qhexrt Documentation\n\nPath: `bindings/flutter/packages/runanywhere_qhexrt/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_qhexrt/README.md)","bindings/flutter/packages/runanywhere_qhexrt/pubspec.yaml":"# Runanywhere_qhexrt Skill\n\nPath: `bindings/flutter/packages/runanywhere_qhexrt/pubspec.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_qhexrt/pubspec.yaml)","bindings/react-native/packages/core/README.md":"# core Documentation\n\nPath: `bindings/react-native/packages/core/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/core/README.md)","bindings/react-native/packages/core/nitro.json":"# Core Skill\n\nPath: `bindings/react-native/packages/core/nitro.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/core/nitro.json)","bindings/react-native/packages/core/package.json":"# Core Skill\n\nPath: `bindings/react-native/packages/core/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/core/package.json)","bindings/react-native/packages/core/tsconfig.json":"# Core Skill\n\nPath: `bindings/react-native/packages/core/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/core/tsconfig.json)","bindings/react-native/packages/core/tsconfig.test.json":"# Core Skill\n\nPath: `bindings/react-native/packages/core/tsconfig.test.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/core/tsconfig.test.json)","bindings/react-native/packages/llamacpp/README.md":"# llamacpp Documentation\n\nPath: `bindings/react-native/packages/llamacpp/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/llamacpp/README.md)","bindings/react-native/packages/llamacpp/nitro.json":"# Llamacpp Skill\n\nPath: `bindings/react-native/packages/llamacpp/nitro.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/llamacpp/nitro.json)","bindings/react-native/packages/llamacpp/package.json":"# Llamacpp Skill\n\nPath: `bindings/react-native/packages/llamacpp/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/llamacpp/package.json)","bindings/react-native/packages/llamacpp/tsconfig.json":"# Llamacpp Skill\n\nPath: `bindings/react-native/packages/llamacpp/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/llamacpp/tsconfig.json)","bindings/react-native/packages/mlx/README.md":"# mlx Documentation\n\nPath: `bindings/react-native/packages/mlx/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/mlx/README.md)","bindings/react-native/packages/mlx/package.json":"# Mlx Skill\n\nPath: `bindings/react-native/packages/mlx/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/mlx/package.json)","bindings/react-native/packages/mlx/tsconfig.json":"# Mlx Skill\n\nPath: `bindings/react-native/packages/mlx/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/mlx/tsconfig.json)","bindings/react-native/packages/onnx/README.md":"# onnx Documentation\n\nPath: `bindings/react-native/packages/onnx/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/onnx/README.md)","bindings/react-native/packages/onnx/nitro.json":"# Onnx Skill\n\nPath: `bindings/react-native/packages/onnx/nitro.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/onnx/nitro.json)","bindings/react-native/packages/onnx/package.json":"# Onnx Skill\n\nPath: `bindings/react-native/packages/onnx/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/onnx/package.json)","bindings/react-native/packages/onnx/tsconfig.json":"# Onnx Skill\n\nPath: `bindings/react-native/packages/onnx/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/onnx/tsconfig.json)","bindings/react-native/packages/qhexrt/README.md":"# qhexrt Documentation\n\nPath: `bindings/react-native/packages/qhexrt/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/qhexrt/README.md)","bindings/react-native/packages/qhexrt/nitro.json":"# Qhexrt Skill\n\nPath: `bindings/react-native/packages/qhexrt/nitro.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/qhexrt/nitro.json)","bindings/react-native/packages/qhexrt/package.json":"# Qhexrt Skill\n\nPath: `bindings/react-native/packages/qhexrt/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/qhexrt/package.json)","bindings/react-native/packages/qhexrt/tsconfig.json":"# Qhexrt Skill\n\nPath: `bindings/react-native/packages/qhexrt/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/qhexrt/tsconfig.json)","bindings/react-native/packages/qhexrt/tsconfig.test.json":"# Qhexrt Skill\n\nPath: `bindings/react-native/packages/qhexrt/tsconfig.test.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/qhexrt/tsconfig.test.json)","bindings/web/packages/core/README.md":"# core Documentation\n\nPath: `bindings/web/packages/core/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/core/README.md)","bindings/web/packages/core/package.json":"# Core Skill\n\nPath: `bindings/web/packages/core/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/core/package.json)","bindings/web/packages/core/tsconfig.json":"# Core Skill\n\nPath: `bindings/web/packages/core/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/core/tsconfig.json)","bindings/web/packages/core/tsconfig.test.json":"# Core Skill\n\nPath: `bindings/web/packages/core/tsconfig.test.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/core/tsconfig.test.json)","bindings/web/packages/llamacpp/README.md":"# llamacpp Documentation\n\nPath: `bindings/web/packages/llamacpp/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/llamacpp/README.md)","bindings/web/packages/llamacpp/package.json":"# Llamacpp Skill\n\nPath: `bindings/web/packages/llamacpp/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/llamacpp/package.json)","bindings/web/packages/llamacpp/tsconfig.json":"# Llamacpp Skill\n\nPath: `bindings/web/packages/llamacpp/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/llamacpp/tsconfig.json)","bindings/web/packages/onnx/README.md":"# onnx Documentation\n\nPath: `bindings/web/packages/onnx/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/onnx/README.md)","bindings/web/packages/onnx/package.json":"# Onnx Skill\n\nPath: `bindings/web/packages/onnx/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/onnx/package.json)","bindings/web/packages/onnx/tsconfig.json":"# Onnx Skill\n\nPath: `bindings/web/packages/onnx/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/onnx/tsconfig.json)"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# AGENTS.md\n\nGuidance for AI coding assistants working in this repository.\n\n> **`AGENTS.md` is the real file; each `CLAUDE.md` is a committed symlink to the\n> `AGENTS.md` beside it.** Editing either name edits the same bytes. If you create a new\n> `AGENTS.md` in a directory that doesn't have one, run\n> `bash scripts/validation/gates/check_agents_claude_sync.sh --fix` from the repo root to\n> create its `CLAUDE.md` symlink — `pr-build.yml` fails any tracked `AGENTS.md` missing one.\n> Never hand-create a `CLAUDE.md` as a real file.\n\n## Working conventions\n\n- **Focus on simplicity.** Clean, SOLID-ish separation of concerns; reuse over\n  reinvention. No mock implementations and no new unit tests unless the task explicitly\n  asks for them.\n- **Plan first for non-trivial work.** Write the plan to\n  `thoughts/shared/plans/{descriptive_name}.md`, get it approved before implementing, and\n  keep it updated (append what changed) as you go so the next engineer can pick it up.\n- **Structured types over raw strings.** Enums/sealed classes/data classes for\n  configuration and results — this is enforced per-SDK by lint (ktlint/detekt, SwiftLint,\n  ESLint) so ad hoc strings tend to get caught anyway.\n- **Read files fully** before editing them; only use offset/limit on files too large to\n  read at once.\n- **Use full local build parallelism** (`-j \"$(sysctl -n hw.logicalcpu)\"` / equivalent),\n  scaling down only under real memory/thermal pressure — not because load average looks\n  high.\n- **Multi-agent workflows run on Sonnet.** When fanning work out to subagents (the\n  `Workflow` tool, or several parallel `Agent` calls), pass `model: 'sonnet'` on every\n  `agent(...)` call rather than letting them inherit the session model. A fan-out of\n  8-10 probes reading large files is exactly the shape where Sonnet is the right\n  cost/latency trade, and these agents are doing bounded, well-specified retrieval and\n  analysis rather than open-ended reasoning. Reserve the session's stronger model for\n  the synthesis step that consumes their reports, and for anything where a wrong answer\n  is expensive and hard to detect — a final design decision, an adversarial verification\n  pass, or a security-relevant judgement.\n\n  ```js\n  // in a Workflow script\n  const findings = await parallel(LENSES.map((l) => () =>\n    agent(l.prompt, { label: `probe:${l.key}`, schema: FINDINGS, model: 'sonnet' })))\n\n  // the synthesis that reads them can stay on the session model\n  const plan = await agent(synthesisPrompt, { schema: PLAN, effort: 'high' })\n  ```\n\n## The most important architectural rule: logic lives at the lowest layer that serves everyone\n\nEach feature/modality (LLM, STT, TTS, VAD, VLM, RAG, LoRA, Voice) is invoked through\n**one** SDK entry point; the SDK, and below it C++ commons, does all the heavy lifting. If\nan example app builds a multi-step bootstrap sequence, hardcodes a model/engine constant,\nor post-processes model output, that is a bug in the SDK, not the app — fix it down a\nlayer.\n\n1. **C++ commons (`core/`)** — cross-platform, non-I/O logic: model lifecycle, registry,\n   download orchestration, RAG session management, inference routing. All 5 SDKs get the\n   fix for free.\n2. **Platform SDK layer** — platform-specific I/O or runtime bridging only (Web OPFS,\n   iOS Keychain, Android Keystore, WASM MEMFS mirroring).\n3. **Example apps** — UI rendering, navigation, thin SDK calls. No business logic, no\n   internal-SDK knowledge (path patterns, framework→directory mappings), no workarounds.\n\n**iOS is the source of truth.** When behavior is ambiguous in any other SDK, check the\niOS Swift implementation first and copy the logic exactly, adapting only syntax.\n\n---\n\n## Repository overview\n\nCross-platform on-device AI SDK monorepo. A single C/C++ core (`runanywhere-commons`,\n~118K first-party LOC plus ~420K generated proto bindings) implements all AI business\nlogic behind a pure C ABI (`rac_*` prefix). Platform SDKs are thin bridges that supply\nplatform services (file I/O, HTTP, Keychain, audio) via an inversion-of-control struct and\ncall into the C core for all inference. Protobuf IDL schemas generate type-safe bindings\nfor every language.\n\n**Current version**: `0.20.37` (canonical source: `core/VERSION`)\n\n| SDK | Path | Bridge mechanism | Platforms |\n|-----|------|-------------------|-----------|\n| Swift | `bindings/swift/` | XCFramework + CRACommons module map | iOS 17.5+, macOS 14.5+ |\n| Kotlin | `bindings/kotlin/` | JNI (`librunanywhere_jni.so`) | Android (min API 24) |\n| Flutter | `bindings/flutter/` | Dart FFI | iOS, Android |\n| React Native | `bindings/react-native/` | NitroModules (JSI HybridObject) | iOS 17.5+, Android arm64 |\n| Web | `bindings/web/` | Emscripten WASM + TypeScript | Chrome, Safari, Firefox |\n| Electron | `bindings/electron/` | Node-API addon | Windows x64 (preview) |\n| Python | `bindings/python/` | pybind11 extension (`runanywhere._core`) | Windows, macOS, Linux (alpha) |\n\nEach has its own `AGENTS.md` with full architecture, build commands, and conventions —\nread it before working in that SDK.\n\n| Native dir | Contents |\n|-----------|----------|\n| `core/` | C/C++ core library: all AI logic, plugin registry, event system |\n| `engines/` | 7 backend plugins: llamacpp, sherpa, onnx, cloud, mlx, qhexrt, neurt |\n| `runtimes/` | 3 runtime adapters: cpu (always), onnxrt, coreml |\n| `idl/` | 39 Protobuf schemas + per-language codegen scripts |\n\n### Consumer apps live in separate repos\n\nThe four full consumer apps were extracted into standalone repos (history preserved); PR\nagainst them there, not here: [runanywhere-ios](https://github.com/RunanywhereAI/runanywhere-ios),\n[runanywhere-android](https://github.com/RunanywhereAI/runanywhere-android),\n[runanywhere-web](https://github.com/RunanywhereAI/runanywhere-web),\n[runanywhere-electron](https://github.com/RunanywhereAI/runanywhere-electron).\n\nTwo example apps remain in-tree (`bindings/flutter/example/`, `bindings/react-native/example/`).\nAll example apps share brand orange `#FF6900`, documented in `docs/DESIGN_GUIDELINE.md`.\n\n### Minimal examples are how you verify an SDK change locally\n\nEach in-repo example consumes its SDK from local source (no staging/publishing needed to\nsee an edit): Swift via `RUNANYWHERE_USE_LOCAL_NATIVES=1`, Kotlin via a Gradle composite\nbuild (`includeBuild` + `dependencySubstitution`), Web via Vite aliases into\n`packages/*/src`. They're deliberately minimal (one prompt in, one streamed completion\nout) — feature-complete UI belongs in the consumer repos above.\n\n---\n\n## Cross-platform architecture\n\n`idl/*.proto` is the schema root; `idl/codegen/generate_all.sh` emits per-language\nbindings (Swift, Kotlin/Wire, TS/ts-proto, Dart), all generated and none committed — see\n[`docs/reference/generated-code-contract.md`](docs/reference/generated-code-contract.md)\nfor the full \"what generates what, when\" contract before touching `idl/`.\n\nAll SDKs reach `runanywhere-commons` through the `rac_*` C API. Commons holds the\ncomponent layer (lifecycle), the service layer (dispatch), and the plugin registry, and\nreaches engines through `rac_engine_vtable_t`.\n\n| Engine | Primitives |\n|---|---|\n| llamacpp | LLM, VLM |\n| sherpa-onnx | STT, TTS, VAD |\n| onnx | Embed, Segment |\n| qhexrt | Hexagon NPU |\n| neurt, cloud | Apple Neural Engine, HTTP |\n\nBackend base priorities (highest wins per primitive via `rac_plugin_find()`, no\nruntime/format scoring): qhexrt=150 (QNN-context models only), mlx=110 (Apple),\nllamacpp=100, sherpa=90, onnx/cloud=50. An explicit engine name is honored through\n`rac_plugin_find_for_engine()` regardless of priority.\n\n### Non-obvious invariants that span every SDK\n\n- **Platform adapter IoC**: `rac_platform_adapter_t` is a flat C struct of function\n  pointers populated by each SDK before `rac_init()`. C++ never calls platform APIs\n  directly — file I/O, HTTP, Keychain, logging, memory queries all pass through it.\n- **Two-phase init**: every SDK does Phase 1 (sync: register adapter, load native libs,\n  configure logging) then Phase 2 (async: authenticate, register device, fetch model\n  assignments, discover downloaded models).\n- **Plugin ABI**: `RAC_PLUGIN_API_VERSION = 9u`; a version mismatch is an immediate\n  rejection. Each vtable has 10 active primitive slots and 7 reserved. NULL slot = not\n  supported. `rerank_ops` (wire value 11) was promoted from `reserved_slot_2` in ABI v8;\n  wire value 6, retired in ABI v4, stays permanently retired — never reuse it. See\n  `core/AGENTS.md` for the full ABI history and how to add a primitive.\n- **Static vs. dynamic plugins**: iOS and WASM force `RAC_STATIC_PLUGINS=ON` (no\n  `dlopen`), registered via `RAC_STATIC_PLUGIN_REGISTER(name)` + `-force_load` /\n  `--whole-archive`. Android/Linux/macOS default to dynamic loading via\n  `rac_registry_load_plugin()`.\n- **Streaming fan-out**: C++ allows only one proto-byte callback per component handle.\n  Each SDK implements its own fan-out to multiplex that into multiple subscribers (Swift\n  `AsyncStream`, Kotlin `Flow`, Dart `StreamController`, TS `AsyncIterable`).\n- **Proto types are canonical**: never hand-write enum values or structured types that\n  exist in `idl/*.proto` — use the generated types/typealiases and regenerate instead.\n- **HTTP is platform-provided**: no libcurl. Each SDK registers a\n  `rac_http_transport_ops_t` (URLSession on Apple platforms, OkHttp on Android,\n  `emscripten_fetch` on Web).\n- **NeuRT crosses a repo boundary as BYTES, not source**: the Apple engine is built and\n  published by the private `neurun` repo and consumed here as prebuilt archives, pinned by\n  tag + SHA-256 in `core/VERSIONS` (`NEURT_*`). Fetch with `scripts/build/download-neurt.sh`.\n  Each archive carries a receipt naming the `RAC_PLUGIN_API_VERSION` it was built against,\n  and both the downloader and CMake refuse a mismatch — a vtable-layout change relinks\n  cleanly and corrupts dispatch at runtime, so nothing else would catch it.\n\nA side-by-side comparison of entry point / bridge / streaming / events / storage / HTTP\nper SDK — useful when porting a fix across SDKs — lives in\n[`docs/reference/cross-sdk-parity.md`](docs/reference/cross-sdk-parity.md).\n\n---\n\n## Building and running\n\nThe root `CMakeLists.txt` (version from `core/VERSION`) is the single entry point for\nnative builds; `CMakePresets.json` defines `macos-{debug,release}`, `linux-{debug,release,asan}`,\n`ios-{device,simulator}`, `android-arm64`, `wasm`, `cpp-desktop-*`, and the `windows-*` presets.\n\n```bash\ncmake --preset macos-debug && cmake --build build/macos-debug && ctest --preset macos-debug\n```\n\n**`./run <group> [subcommand]`** is the unified wrapper for everything else — build,\nlint, and run any SDK or example app from one CLI (`./run --help` for the full menu):\n\n```bash\n./run doctor                    # scan host toolchains, show what's buildable here\n./run setup                     # provision env for every host-buildable target\n./run sdk commons build-android # build C++ commons for all Android ABIs, stage .so files\n./run sdk kotlin build          # (etc. — see per-SDK AGENTS.md for direct tool commands)\n./run example android install   # build + install + launch the Kotlin minimal example\n./run codegen                   # regenerate every language binding from idl/\n```\n\nFor the direct `swift build` / `./gradlew` / `melos` / `yarn` / `npm` commands each SDK\nuses under the hood, and its test/lint/publish targets, see that SDK's own `AGENTS.md`\n(`bindings/<name>/AGENTS.md`) — the commands are kept there, not duplicated here, so they\ncan't drift.\n\nCross-platform build scripts worth knowing about at the root level:\n\n```bash\n./bindings/swift/scripts/build-core-xcframework.sh   # also syncs into RN/Flutter plugin dirs\n./scripts/build/build-core-android.sh                # .so for all ABIs → every SDK's jniLibs/\n./bindings/web/scripts/build-core-wasm.sh\n./scripts/setup/setup-toolchain.sh                   # protoc/wire/ts-proto toolchain\n./idl/codegen/generate_all.sh [--only <lang>]         # swift|kotlin|dart|ts|cpp|python\n```\n\n---\n\n## Version management and releases\n\nCanonical version: `core/VERSION` (single-line semver). Bump everywhere with:\n\n```bash\n./scripts/release/sync-versions.sh <version>\n```\n\nThe full release runbook (version bump → PR → tag → GitHub Release → npm/Maven/pub.dev/\nSwiftPM publishing → cutting the `runanywhere-swift` SPM distribution repo → starter-app\nsmoke tests) is a multi-step process captured in three skills — use them rather than\nimprovising the steps: **sdk-release** (version bump through published GitHub Release),\n**sdk-publish** (registry publishing + the `runanywhere-swift` dist repo cut), and\n**sdk-test-starters** (post-release device/app smoke tests).\n\n### Release pipeline lessons (v0.20.29 through v0.20.33 — read before cutting a release by hand)\n\n- **`native_ios` and `native_android` always rebuild fresh archives on every `release.yml`\n  run — there is no reuse/skip mechanism for either job** (unlike `native_web`'s\n  `reuse_native_web_run_id` or the cpp-desktop kits' `reuse_cpp_desktop_run_id`). This makes\n  the Apple/Android TEMP-pin convention (Package.swift's `sdkVersion`, the Flutter podspecs'\n  `asset_version`, Kotlin's `nativeLibVersion` pinned to an old version \"because this release\n  ships no new iOS/Android archives\") **structurally unsound for any release where those two\n  jobs actually run — which is every release.** It surfaced as three separate bugs\n  (`auto-tag.yml`'s coherence check, `package-sdk.sh`'s podspec check, and `sync-checksums.sh`'s\n  publish-time verification) before the pattern was recognized. If a release genuinely ships\n  no new Apple/Android bytes, that has to be proven by diffing the actual built artifacts\n  against the previous release, not assumed from what changed in the diff.\n- **`RunAnywhereMLXRuntime`'s stripped Mach-O is not byte-reproducible across builds**\n  (`RAC_CHECKSUM_SKIP=RunAnywhereMLXRuntime` in `release.yml`'s publish job exists because of\n  this, and is the ONLY archive exempted from the publish-time checksum gate). The corollary\n  that bit v0.20.29: **the checksum committed to `Package.swift` / the Flutter MLX podspec\n  can only be computed from a build that already happened, but the tag that freezes those\n  files has to be pushed before `native_ios` (which produces that exact artifact) even runs**\n  — so re-syncing checksums against an EARLIER, superseded build (e.g. after a tag had to be\n  moved and re-dispatched to fix an unrelated bug) commits a value that does not match what\n  the FINAL successful run actually publishes, and the skip means nothing catches it. v0.20.29\n  shipped this way (committed `d942788f...`, actually published `fc4b23a0...`) — caught only\n  because the Flutter `runanywhere_mlx` package hadn't been published to pub.dev yet for that\n  version. Before publishing any package that ships `RunAnywhereMLXRuntime`, re-run\n  `bindings/swift/scripts/sync-checksums.sh` against the artifacts from the SPECIFIC run whose\n  tag is being published — never reuse a checksum computed against an earlier attempt.\n- **The Electron QHexRT backend never set `ADSP_LIBRARY_PATH`, and one comment in the\n  codebase confidently asserted Windows doesn't need it.** It does, for exactly one decode\n  path: `QHexRT/src/bonsai/fastrpc_win.cpp`'s custom FastRPC skel (the ternary/Bonsai\n  decoder, e.g. `qwen3.8-27b-1bit-npu`) resolves through `ADSP_LIBRARY_PATH ∪ cwd`, separately\n  from the standard QNN HTP graph path's `LoadLibraryW` + PATH search that `bridge.ts`'s\n  `addSidecarDirToDllSearch` already handled. Every OTHER QHexRT model worked fine in the\n  packaged app, which is exactly what let this ship undetected — validate the Bonsai/ternary\n  path specifically, not just \"some NPU model works.\" Fixed in `bridge.ts`'s\n  `addSidecarDirToDspSearchPath` (#803).\n- **`package-private-engine-overlay.sh`'s QAIRT-runtime copy step filtered on `{\".dll\",\n  \".lib\"}` only**, silently dropping every `.so`/`.cat` skel file (both the standard HTP skel\n  and, since it never read `engines/qhexrt/prebuilt/current/dsp/win-arm64/` at all, the\n  Bonsai skel too). This meant **RCLI could not have run any QHexRT model** — standard or\n  ternary — from a fresh private overlay, a gap invisible until someone actually tried to run\n  inference rather than just building. `bindings/electron/scripts/bundle-native.ts` already\n  had the correct pattern (copy `dsp/win-arm64/` flat, no extension filter); the overlay\n  script just never matched it. Fixed in #804.\n- **A new NeuRT archive must be added in FOUR places, and three of them only fail in a build\n  configuration your Mac never produces.** v0.20.33 added `libneurt_rac_tts_ops.a` and took two\n  wasted release candidates to land, because each site fails somewhere different and late:\n  `engines/neurt/CMakeLists.txt` (probe list + imported target + `_neurt_extra_links`),\n  `build-core-xcframework.sh` (pre-flight guard AND `_NEURT_ROUTABLE_ARCHIVES` — two lists in one\n  file), `package-private-engine-overlay.sh`, and the `#if RAC_NEURT_ROUTABLE` guard around the\n  vtable entry in `rac_plugin_entry_neurt.cpp`. Miss the force-load lists and every native job goes\n  green while `validate_consumer_flutter` dies an hour later on `Undefined symbol:\n  _g_neurt_tts_ops` — the engine registers, all CMake targets compile, and only a CONSUMER's link\n  fails. Miss the vtable guard and the routable build is fine while every PR job WITHOUT\n  `NEURUN_TOKEN` fails, because a non-routable shell still references a symbol nothing defines.\n  A dev machine with the payload staged is routable, so it reproduces neither. `libneurt_rac_*.a`\n  archives now fail packaging by name if they are not in the list\n  (`neurt_assert_no_unlisted_archives`); the vtable guard has no such mechanism — audit all ten\n  slots when adding one. Reproduce a non-routable build by moving `core/third_party/neurt` aside.\n- **`reuse_native_web_run_id` is SAME-VERSION-ONLY, whatever the diff looks like.** `racommons.wasm`\n  embeds `RAC_VERSION_STRING` and `validate_public_packages.py` asserts it against the version being\n  packaged, so reusing ANY prior release's run fails `sdk_web` with *\"does not embed\n  RAC_VERSION_STRING\"*. Worse than a red job: `native_web` is skipped, so that candidate never\n  uploads a web artifact and can never serve as `publish_from_run_id`. Only a re-dispatch at the\n  same version qualifies.\n- **Verify checksums by NAME as well as by URL — the manifests use two different shapes.** The root\n  `Package.swift` interpolates `\\(sdkVersion)` into each URL; the Flutter manifests bind a checksum\n  to a `name:` through a helper with no URL at the call site at all. A URL-only cross-check silently\n  validates 9 of 15 sites and reports success. Re-hash every site against the zip it names, and\n  remember `RAC_CHECKSUM_SKIP` hides `RunAnywhereMLXRuntime` from `publish` entirely, so the Flutter\n  MLX podspec is verifiable only by hand.\n- **A green `rcli backends --json` proves nothing against the OSS kit.** NeuRT ships as a separate\n  private pack, so the public bottle lists no `neurt` at all — a routability check there passes\n  cleanly and means nothing. Apply the private overlay\n  (`cpp-desktop-macos-arm64-neurt-private` workflow artifact) before believing the result.\n- **General pattern across all four of the above**: each one was invisible to the checks that\n  existed (a version-coherence check, a checksum-skip escape hatch, a \"the model loaded\"\n  smoke test, a \"the build succeeded\" CI job) because none of those checks actually ran the\n  specific thing that was broken. When validating a release, run the SPECIFIC flagship/hardest\n  model end-to-end on-device, not just confirm the pipeline completed.\n- **The RACommons/RunAnywhereMLXRuntime checksum-staleness bug above recurred on v0.20.30 AND\n  v0.20.31** — same `mismatch: RACommonsBinary` failure in `publish`, same root cause both\n  times: the version-bump PR ran `sync-versions.sh` and merged/tagged directly, skipping the\n  **mandatory** \"dispatch a release candidate off the unmerged branch, sync checksums from\n  its real build, THEN merge\" sequence that the **`sdk-release` skill's step 2-3 already\n  document in detail** (including this exact failure signature). Documenting the lesson in\n  prose here was not enough to stop it recurring twice more — **if you are about to run\n  `scripts/release/sync-versions.sh` for ANY reason, including as one step of a larger\n  cross-repo release cascade, invoke the `sdk-release` skill and follow it from step 1**,\n  rather than reconstructing the sequence from memory or treating a \"small\" version bump as\n  exempt. v0.20.31 additionally caught `RunAnywhereMLXRuntime` in the Flutter podspec\n  independently stale in the SAME commit — `RAC_CHECKSUM_SKIP` exempts it from `publish`'s\n  check, so it would have shipped broken to Flutter consumers with no CI signal at all; check\n  every row of the checksum-location table, not only the one the CI error names.\n- **QHexRT's `qhx_runtime_device()` (arch/soc lookup, needed to pick the `v75`/`v79`/`v81`\n  manifest directory in `session_open()`) shared its live-device cost with the standard QNN\n  graph path**, so a `host_only` model with ZERO QNN graphs — the Bonsai/Maple ternary decoder\n  (`qwen3.8-27b-1bit-npu`) — paid for a live QNN HTP device anyway, which then contended with\n  the decoder's own direct FastRPC session for the same Hexagon cDSP and made every generation\n  fail with a bare `HostOpFailed`. This shipped in v0.20.30 (the SAME release that fixed the\n  `ADSP_LIBRARY_PATH` bug for the same model) and was found only by running that SPECIFIC model\n  on real Windows-ARM64 hardware after the \"fix\" — reinforcing the point above: a green\n  `HostOpFailed`-free run of a DIFFERENT QHexRT model proves nothing about this one. Two red\n  herrings were chased and ruled out with live device tracing before the real cause: neither\n  `ADSP_LIBRARY_PATH` (confirmed correctly set) nor a QAIRT-version mismatch between the\n  shipped host DLLs and the on-device skel (confirmed present, but fixing it alone did not fix\n  the failure) was the actual cause. Fixed in `neurun` v0.20.31 (`Backend::profile()` now uses\n  a separate, device-handle-free `deviceGetPlatformInfo` query instead of sharing\n  `ensure_device()` with `device()`) + this repo's `qhexrt_session.cpp` (#810, a complementary\n  fix removing a second, redundant eager device query used only for a log line).\n\n---\n\n## CI/CD workflows (`.github/workflows/`)\n\n| Workflow | Trigger | Purpose |\n|----------|---------|---------|\n| `pr-build.yml` | PR to main, push to main/feat branch | Parallel native builds (macOS/Linux/iOS/Android) + per-SDK typecheck + centralization/coherence gates |\n| `release.yml` | Tag `v*.*.*` or manual | Full artifact build matrix, SDK packaging, consumer validation, draft Release |\n| `auto-tag.yml` | PR merged to main with `release:*` label | Verifies the reviewed semver bump, pushes that exact git tag |\n| `idl-drift-check.yml` | Changes to `idl/` or generated files | Regenerates protos, verifies schema lock + untracked generated trees |\n| `legacy-files-blocklist.yml` | All PRs/pushes | Prevents specific deleted files from being re-introduced |\n| `secret-scan.yml` | PRs and pushes to main | Incremental gitleaks scan on diff range |\n| `check-no-pii-logging.yml` | PRs/pushes to main/master/feat-branch | Guards against logging signed URLs alongside active-download destination paths |\n\n---\n\n## Platform requirements\n\n| Platform | Min version | Build tool | Key versions |\n|----------|------------|------------|--------------|\n| iOS / macOS | 17.5 / 14.5 | Xcode 26+ | Swift 6.2 |\n| Kotlin SDK | Android API 24 | AGP 9.2.1 / Gradle 9.5.0 | Kotlin 2.4.0, NDK 27.3.13750724 |\n| Flutter | 3.44.6 | Melos / AGP 9.0.1 / Gradle 9.1.0 | Dart 3.12.2+, NDK 28.2.13676358 |\n| React Native | 0.85.3 (min 0.83.1) | Yarn Berry 3.6.1 | NitroModules, Hermes |\n| Web | Chrome 86+ | Vite | Emscripten 6.0.2, Node 24 LTS |\n| C++ core | N/A | CMake 3.24+ | C++20, Ninja |\n\n---\n\n## Non-obvious configuration details\n\n- **NDK pin**: `core/VERSIONS::NDK_VERSION` (27.3.13750724) is the single source of truth,\n  mirrored into `bindings/kotlin/gradle.properties`. NDK 27 (r27d) gives 16 KB\n  page-alignment required by Android 15+ — NDK 25.x's 4 KB-aligned `libc++_shared.so` /\n  `libomp.so` trips Android 16's page-size enforcement. Mirror this pin whenever bumping\n  it; Flutter/RN Android build files carry their own fallback literals that can drift.\n- **`useLocalNatives`** (Kotlin `gradle.properties`, similarly named flags elsewhere):\n  `true` builds native libs from source locally; CI and most non-local runs set it\n  `false` to download prebuilt `.so`/`.xcframework`/`.wasm` from GitHub Releases instead.\n- **Web SDK** has several hard-won WASM/browser runtime workarounds (VLM worker crash\n  recovery, a Qwen2-VL WebGPU NaN bug, cross-origin isolation requirements) — see\n  [`docs/reference/web-runtime-gotchas.md`](docs/reference/web-runtime-gotchas.md).\n- **Cloud/Linux dev VM** (e.g. Cursor Cloud): what's buildable there and the\n  clang/NDK/KVM gotchas are in\n  [`docs/reference/cursor-cloud-environment.md`](docs/reference/cursor-cloud-environment.md).\n\n---\n\n## Pre-commit hooks\n\n```bash\npre-commit run --all-files                     # everything\npre-commit run ios-sdk-swiftlint --all-files   # SwiftLint only\n```\n\nConfigured hooks: gitleaks (secrets), trailing-whitespace, end-of-file-fixer, check-yaml,\ncheck-added-large-files (1000 KB max), check-merge-conflict, object file detection,\nSwiftLint (SDK + example app), periphery (unused Swift code detection).\n\n---\n\n## Where to look for current in-flight work\n\n`thoughts/shared/plans/` holds active/completed execution plans (including any in-flight\nSDK reorg or migration briefs — e.g. the Electron reorganization plan); `thoughts/shared/issues/`\nholds tracked bug write-ups. Check these directories directly rather than trusting a\nstatus summary written into this file — both change too often for a static snapshot here\nto stay accurate.\n","isInternal":false,"tokens":6705,"sizeBytes":26163},{"name":"AGENTS.md","path":"core/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/core/AGENTS.md","title":"core - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# core - AI Agent Protocol & Instructions\n\nPath: `core/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/core/AGENTS.md)","tokens":300,"sizeBytes":181},{"name":"AGENTS.md","path":"engines/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/engines/AGENTS.md","title":"engines - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# engines - AI Agent Protocol & Instructions\n\nPath: `engines/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/engines/AGENTS.md)","tokens":300,"sizeBytes":190},{"name":"AGENTS.md","path":"idl/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/idl/AGENTS.md","title":"idl - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# idl - AI Agent Protocol & Instructions\n\nPath: `idl/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/idl/AGENTS.md)","tokens":300,"sizeBytes":178},{"name":"AGENTS.md","path":"runtimes/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/runtimes/AGENTS.md","title":"runtimes - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# runtimes - AI Agent Protocol & Instructions\n\nPath: `runtimes/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/runtimes/AGENTS.md)","tokens":300,"sizeBytes":193},{"name":"AGENTS.md","path":"scripts/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/scripts/AGENTS.md","title":"scripts - AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# scripts - AI Agent Protocol & Instructions\n\nPath: `scripts/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/scripts/AGENTS.md)","tokens":300,"sizeBytes":190},{"name":"SKILL.md","path":".agents/skills/engine-plugin-authoring/SKILL.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/.agents/skills/engine-plugin-authoring/SKILL.md","title":"engine-plugin-authoring","category":"anthropic-skill","format":"markdown","content":"---\nname: engine-plugin-authoring\ndescription: Step-by-step recipe for adding a new engine plugin under runanywhere-sdks/engines/, or adding a modality (e.g. TTS) to an existing engine (llamacpp, sherpa, onnx, cloud, mlx, neurt, qhexrt). Use when the task is \"add engine X\", \"wire up backend Y\", or \"make engine Z also serve <modality>\".\n---\n\n# Engine plugin authoring (`engines/`)\n\nTwo related runbooks: adding a brand-new engine plugin, and adding a modality to one\nthat already exists. Read `engines/AGENTS.md` first for the concepts these steps\nassume (op-table vtable, manifest, the valid-engine checklist, the 4-file skeleton,\nthe engine↔runtime rule). This skill is the \"how\", that file is the \"what/why\".\n\n**Invariant that applies to both procedures:** never invent a new modality just to\nship an engine — fill an existing `rac_engine_vtable_t` slot. Adding a brand-new\n*primitive* (a new `RAC_PRIMITIVE_*` + vtable slot) is a commons ABI change, covered\nby `core/AGENTS.md` → \"Adding a new capability interface\", not an engine change.\n\n---\n\n## Procedure A — add a new engine\n\nMirror **sherpa** (multi-modality, standalone `.so`) or **cloud** (single modality,\nno runtime) as your template; both are the cleanest references for the 4-file\nskeleton described in `engines/AGENTS.md`.\n\n1. **Create `engines/<name>/`** with the 4-file skeleton: `rac_plugin_entry_<name>.cpp`,\n   `rac_backend_<name>_register.cpp` (skip it if there's no extra bring-up, like\n   `neurt`), `rac_static_register_<name>.cpp`, plus your impl files.\n2. **Write the manifest** in `rac_plugin_entry_<name>.cpp`:\n   - `name` = snake_case identity (the library/framework you wrap, or your own\n     codebase unit — NEVER a modality name; see the naming rule in\n     `engines/AGENTS.md`).\n   - `primitives[]` = what you serve; fill the matching vtable slots; leave every\n     other slot explicit NULL (the all-NULL tripwire depends on this).\n   - `runtimes[]` — only devices your compute actually depends on (NULL for\n     cloud/HTTP engines) — see THE RULE in `engines/AGENTS.md`.\n   - `formats[]` = the `RAC_MODEL_FORMAT_ID_*` values you accept (NULL if there's\n     no local model file).\n   - `priority`, `availability` (PUBLIC/PRIVATE), `package_owner`/`package_name`.\n3. **Implement the op-table(s)** honoring the MODEL LIFECYCLE:\n   `create → initialize → use → cleanup → destroy` (see `engines/AGENTS.md` for the\n   exact signatures). Use `RAC_DEFINE_CREATE_ADAPTER(primitive, name)`\n   (`core/include/rac/plugin/rac_plugin_entry.h`) when `create` is a plain forward\n   onto `rac_<primitive>_<name>_create` — sherpa uses it for STT/TTS/VAD. Engines\n   with richer create flows (llamacpp, onnx, neurt, qhexrt) hand-write it.\n4. **`capability_check`** when the engine is platform- or binary-gated: use the\n   shared 3-way helper `rac_engine_unavailable_capability(platform_supported,\n   backend_present)` from `engines/common/rac_engine_unavailable.h`. When the\n   backend can be entirely absent at build time (a private prebuilt archive, a\n   sibling private repo), emit the stub with `RAC_ENGINE_UNAVAILABLE_PLUGIN(name,\n   display, cap_fn)` from the same header — see qhexrt and neurt for the two-mode\n   (ROUTABLE vs STUB) pattern this produces.\n5. **Registration carriers:** `RAC_PLUGIN_ENTRY_DEF(<name>)` returning the vtable; a\n   `rac_backend_<name>_register()`/`_unregister()` pair if you have extra bring-up\n   (called directly by SDK bridges on dynamic-link hosts); a\n   `rac_static_register_<name>.cpp` shim using `RAC_STATIC_REGISTER_BACKEND(<name>)`\n   (routes through the register fn) or `RAC_STATIC_PLUGIN_REGISTER(<name>)` (calls\n   the entry directly) for iOS/WASM static hosts.\n6. **CMake:** call `rac_add_engine_plugin(<name> SOURCES … LINK_LIBRARIES …\n   AVAILABILITY … PACKAGE_OWNER … PACKAGE_NAME …)` (`cmake/plugins.cmake`) from\n   `engines/<name>/CMakeLists.txt`, fronted by your own\n   `option(RAC_BACKEND_<NAME> \"…\" <default>)` + `if(NOT RAC_BACKEND_<NAME>)\n   return() endif()` self-gate (each engine owns its own option — `engines/CMakeLists.txt`\n   just unconditionally `add_subdirectory()`s and lets the child decide). Add\n   `add_subdirectory(<name>)` there. Declare `primitives`/`runtimes`/`formats` ONLY\n   in the C manifest, never in CMake, so the build graph can't drift from what the\n   router routes.\n7. **Android:** if the SDK loads the engine via `System.loadLibrary` +\n   `nativeRegister`, add a JNI bridge with `RAC_DEFINE_ENGINE_JNI_BRIDGE(...)`\n   (standalone per-engine `.so` — onnx, llamacpp) or\n   `RAC_DEFINE_ENGINE_JNI_BRIDGE_NO_ONLOAD(...)` (folded into a host lib that\n   already owns `JNI_OnLoad`, e.g. cloud → `librunanywhere_jni.so`) from\n   `engines/common/rac_engine_jni_bridge.h`. The JVM class-path token in the macro\n   call must match the Kotlin `*Bridge` class byte-for-byte.\n8. **iOS/WASM static hosts:** make sure the host force-loads the plugin —\n   `rac_force_load(<app> PLUGINS <name>)` in the host's CMakeLists — so the\n   static-init Registrar TU survives linker stripping.\n9. **If this engine ships an Electron/npm carrier package**, register it in\n   `scripts/validation/gates/check_plugin_natives.py`'s `REQUIRED_OPS` dict (the\n   ops-table symbol name(s) your entry TU references only on the routable\n   branch) — `package-sdk.sh` calls this automatically, so an unregistered\n   engine's stub/shell builds ship undetected. **Check which FILE actually\n   compiles `rac_plugin_entry_<name>.cpp`** before assuming the carrier\n   (`runanywhere_<name>`) is what to gate: sherpa/onnx/llamacpp compile it into\n   the carrier itself (an undefined symbol reference there is the evidence),\n   but neurt's `rac_add_engine_plugin()` call puts it on the ENGINE target\n   (`rac_backend_neurt`) instead — the carrier there is empty boilerplate with\n   zero symbols. Add the engine id to `EVIDENCE_IN_BACKEND_FILE` if it follows\n   neurt's shape; discovered by actually building neurt for real and running\n   `nm -a` on both files, not by reading the CMakeLists and assuming.\n\n## Procedure B — add a modality to an existing engine\n\nMulti-modality is the whole point of an identity-named engine. `cloud` and `neurt`\nare explicitly built for this and carry the recipe inline as a comment next to\ntheir vtable.\n\n1. Implement the new modality's op-table (e.g. a cloud TTS `g_cloud_tts_ops`,\n   typically backed by a per-modality provider adapter under `providers/`).\n2. **Fill the corresponding slot** in the engine's `rac_engine_vtable_t`\n   (`tts_ops` / `llm_ops` / `embedding_ops` / …) — every other slot must stay\n   explicit NULL.\n3. **Add the primitive** to the manifest's `primitives[]`.\n4. Update `formats[]`/`runtimes[]` only if the new modality actually needs them\n   (per THE RULE — don't declare a runtime your new op-table doesn't depend on).\n\nNo new plugin, no rename, no ABI bump — the engine already owns one\n`rac_engine_vtable_t` with all ten primitive slots; you're just filling another one.\nGrep the target engine's `rac_plugin_entry_<name>.cpp` for the phrase \"To add a\"\n— cloud and neurt both leave a comment naming exactly which slots are still free\nand what to add to `primitives[]`.\n","frontmatter":{"name":"engine-plugin-authoring","description":"Step-by-step recipe for adding a new engine plugin under runanywhere-sdks/engines/, or adding a modality (e.g. TTS) to an existing engine (llamacpp, sherpa, onnx, cloud, mlx, neurt, qhexrt). Use when the task is \"add engine X\", \"wire up backend Y\", or \"make engine Z also serve <modality>\"."},"isInternal":false,"tokens":1903,"sizeBytes":7202},{"name":"SKILL.md","path":".agents/skills/sdk-publish/SKILL.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/.agents/skills/sdk-publish/SKILL.md","title":"sdk-publish","category":"anthropic-skill","format":"markdown","content":"---\nname: sdk-publish\ndescription: Manually publish an already-tagged, already-released (non-draft GitHub Release) RunAnywhere SDK version to every package registry (npm, Maven Central, pub.dev, Swift SPM dist repo) plus macOS rcli notarization, with heavy verification that nothing fake/stubbed/broken ships.\n---\n\n# SDK Publish\n\nRepo: `~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks` (verify with `pwd`/`git remote get-url origin` — do not assume this path is still correct, checkouts move). Version source of truth: `core/VERSION`. Throughout, `$VERSION` means the bare semver being published (e.g. `0.20.24`), never prefixed with `v` except in git tags / GitHub Release names.\n\n## THIS PROCESS IS INTENTIONALLY MANUAL\n\n**`release.yml` never publishes anywhere public.** It only builds artifacts, packages them, and creates a draft/published **GitHub Release**. It does not call `npm publish`, does not upload to Maven Central, does not call `flutter pub publish`, and does not push the Swift dist repo. Verify this is still true before trusting it — greps below should all return **zero** matches (grep the whole file, not a snippet):\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks\ngrep -inE \"npm publish|publishReleasePublication|publishAllPublicationsToMavenCentral|flutter pub publish\" .github/workflows/release.yml\n```\n\nThis is a **deliberate safety gate**, not a gap. Every step below is a human (or an agent acting on a human's explicit go-ahead) pushing a button that CI intentionally never pushes. Do not propose wiring any of this into CI as a \"fix\" — that would remove the gate.\n\n## Precondition check (always run first)\n\nRefuse to proceed if the release is a draft or the asset set looks incomplete.\n\n```bash\ngh release view v$VERSION --json isDraft,tagName --jq '{isDraft, tagName}'\n# isDraft MUST be false.\n\ngh release view v$VERSION --json assets --jq '.assets[].name' | sort\n```\n\nCross-reference the listing against the `sdk-release` skill's step 8 asset-count expectations (`~/.claude/skills/sdk-release/SKILL.md`). As a second, independent check, also diff against the **previous** tagged release's asset list — same count, same name patterns, only the version token differs:\n\n```bash\ngh release view v<PREVIOUS_VERSION> --json assets --jq '.assets[].name' | sed \"s/<PREVIOUS_VERSION>/$VERSION/\" | sort >/tmp/expected.txt\ngh release view v$VERSION --json assets --jq '.assets[].name' | sort >/tmp/actual.txt\ndiff /tmp/expected.txt /tmp/actual.txt\n```\n\nAs of the last verified run (**v0.20.25, 65 assets** — up from 55 at v0.20.24 because the 5 public Electron tarballs and their sidecars joined the release; see §6), the full set is: `MANIFEST.txt`; per-platform `RACommons-{android-arm64-v8a,android-armeabi-v7a,android-x86_64,ios,linux-x86_64,web,windows-x64}-v$VERSION.{zip,tar.gz}` + `.sha256`; iOS backend zips `RABackend{LLAMACPP,MLX,NeuRT,ONNX,Sherpa}-ios-v$VERSION.zip` + `.sha256`; `RunAnywhereMLX{Metal,Resources,Runtime}-ios-v$VERSION.zip` + `.sha256`; `rcli-{linux-x86_64,macos-arm64,windows-x86_64}-v$VERSION.tar.gz`/`.zip` + `.sha256`; the 8 npm tarballs `runanywhere-{core,llamacpp,mlx,onnx,proto-ts,web,web-llamacpp,web-onnx}-$VERSION.tgz` + `.sha256`; the **5 Electron npm tarballs** `runanywhere-electron{,-llamacpp,-onnx,-sherpa,-neurt}-$VERSION.tgz` + `.sha256` (new as of v0.20.25); and `runanywhere-kotlin-maven-v$VERSION.zip` + `.sha256`. There is **no** Flutter tarball in the release (Flutter packages are staged from the native archives above, not from a dedicated release asset) and **no** `.dmg` asset in the baseline set (the macOS rcli tarball is what ships; see the notarization section for when a `.dmg` does get produced). If a `qhexrt`-named asset appears anywhere in this list, stop — that would mean `release.yml`'s own manifest assertion should have hard-failed and something is very wrong; do not publish anything from that release.\n\n## 0. MANDATORY GATE — verify the ENTIRE release before pushing anything to any registry\n\n**Run this once, in full, before the first registry push. Do not publish a single package until it passes.** Per-package spot-checks are not a substitute: they are how a bad artifact reaches a registry you cannot un-publish from. Proven end to end on v0.20.25 (2026-08-22), then independently audited and corrected: **34 payloads, 6619 files across 3 nesting levels, 282 binaries, 0 placeholder hits, 38/38 commons binaries carrying the real staging host**. The audit found 4 methodology gaps in the first pass (shallow nesting, missing `MTLB` magic, `.lib` excluded from the positive check, and a set-intersection that could pass vacuously) — every one is called out inline below, because each produced a *confident but under-covered* result.\n\n```bash\nVERSION=0.20.25                      # no leading v\nREPO=RunanywhereAI/runanywhere-sdks\nW=/tmp/relverify-$VERSION\nrm -rf \"$W\" && mkdir -p \"$W/assets\" \"$W/qhexrt\" \"$W/x\" \"$W/nested\"\n\n# (1) EVERY release asset — no -p filter, get the whole set.\ngh release download \"v$VERSION\" --repo \"$REPO\" -D \"$W/assets\"\n\n# (2) The PRIVATE QHexRT artifact is NOT a release asset — pull it from the run.\n#     Find the run that produced the release, then:\ngh run download <release-run-id> --repo \"$REPO\" --name sdk-electron-qhexrt-private --dir \"$W/qhexrt\"\n```\n\n### (3) Checksums — `cd` into the directory first\n\nSidecars contain a **bare basename**, so `shasum -c` fails with a misleading \"FAILED open or read\" if run from anywhere else. This will make every artifact look broken when nothing is wrong:\n\n```bash\ncd \"$W/assets\"\nok=0; bad=0\nfor s in *.sha256; do\n  if shasum -a 256 -c \"$s\" >/dev/null 2>&1; then ok=$((ok+1)); else echo \"MISMATCH: $s\"; bad=$((bad+1)); fi\ndone\necho \"verified=$ok mismatched=$bad\"      # bad MUST be 0\n(cd \"$W/qhexrt\" && shasum -a 256 -c ./*.sha256)\n```\n\n### (4) Extract everything — INCLUDING nested archives\n\n**The gotcha that matters:** `runanywhere-kotlin-maven-v*.zip` contains `.aar` files, and the `.aar`s are what hold the Android `.so` binaries. A one-level extraction reports \"0 binaries\" for the Maven bundle and every subsequent scan passes **vacuously**.\n\n**Recurse until the tree is dry — TWO levels are required here, not one.** Each `.aar` also contains a `classes.jar`, holding ~2027 `.class` files that are doubly unreachable: inside an unopened jar *and* excluded by the `.class` suffix rule most magic-byte scanners use. Level 2 adds **2033 files** (0 placeholder hits on v0.20.25); level 3 is empty. Loop until a pass finds no new archives rather than hardcoding a depth.\n\n```bash\ncd \"$W/assets\"\nfor f in *; do\n  case \"$f\" in *.sha256|MANIFEST.txt) continue;; esac\n  d=\"$W/x/$f\"; mkdir -p \"$d\"\n  case \"$f\" in\n    *.zip)              unzip -qq -o \"$f\" -d \"$d\" || echo \"EXTRACT FAILED: $f\" ;;\n    *.tar.gz|*.tgz)     tar -xzf \"$f\" -C \"$d\"     || echo \"EXTRACT FAILED: $f\" ;;\n    *) echo \"UNHANDLED TYPE: $f\" ;;\n  esac\ndone\nd=\"$W/x/$(basename \"$W\"/qhexrt/*.tgz)\"; mkdir -p \"$d\"; tar -xzf \"$W\"/qhexrt/*.tgz -C \"$d\"\n\n# nested pass\ncd \"$W/x\"\nfind . -type f \\( -name '*.aar' -o -name '*.jar' -o -name '*.zip' -o -name '*.tgz' -o -name '*.tar.gz' \\) \\\n  | sed 's|^\\./||' | while IFS= read -r a; do\n    dd=\"$W/nested/$(printf '%s' \"$a\" | tr '/' '_')\"; mkdir -p \"$dd\"\n    case \"$a\" in *.aar|*.jar|*.zip) unzip -qq -o \"$a\" -d \"$dd\";; *) tar -xzf \"$a\" -C \"$dd\";; esac\n  done\n```\n\n**Prove extraction actually happened** — an empty dir makes \"0 hits\" meaningless:\n\n```bash\nfor d in \"$W\"/x/*/; do\n  n=$(find \"$d\" -type f | wc -l)\n  [ \"$n\" -lt 2 ] && echo \"SUSPICIOUSLY EMPTY: $d ($n files)\"\ndone\n```\n\n### (5) Placeholder scan — EVERY file, not just binaries\n\nThe staging URL can also live in JS/JSON/TS config, so scan by content across all file types, and additionally scan the raw compressed streams in case a member never got extracted:\n\n```bash\nfind \"$W/x\" \"$W/nested\" -type f | wc -l                                     # total files covered\ngrep -rla \"YOUR_STAGING_BASE_URL\" \"$W/x\" \"$W/nested\" | tee /tmp/ph.txt | wc -l   # MUST be 0\nfor f in \"$W\"/assets/*.tgz \"$W\"/assets/*.tar.gz \"$W\"/qhexrt/*.tgz; do\n  gzip -dc \"$f\" 2>/dev/null | grep -qa \"YOUR_STAGING_BASE_URL\" && echo \"RAW HIT: $f\"\ndone\n```\n\nFind binaries by **magic bytes, not extension** (an unsuffixed framework binary like `RACommons.framework/RACommons` has no extension and a name-based `find` misses it). Signatures to cover:\n\n| Magic | Kind | v0.20.25 count |\n|---|---|---|\n| `7f454c46` | ELF | 177 |\n| `!<arch>` (`213c617263683e0a`) | static `.a` **and `.lib`** | 41 |\n| `MZ` | PE | 29 |\n| `cffaedfe` / `cefaedfe` | Mach-O | 19 |\n| `00 61 73 6d` | wasm | 10 |\n| **`4d544c42` (`MTLB`)** | **Metal library** — easy to miss | 5 |\n| `cafebabe` / `bebafeca` | fat Mach-O (exclude `.jar`/`.class`) | 1 |\n\n**Total: 282 binaries.** An earlier pass reported 277 because `MTLB` was absent from the signature list — `.metallib` files were invisible to the binary scan (they were still covered by the all-file `grep` in step 5, so the conclusion held, but the count was wrong). If your total comes in materially under ~282 for a comparable release, your detection is leaking.\n\n### (6) POSITIVE staging-URL check — absence of the placeholder is NOT sufficient\n\nA build with an **empty** URL baked in passes a placeholder-only check. `check_plugin_natives.py` only tests absence, so add the positive assertion yourself.\n\nTwo traps that produced false confidence on the v0.20.25 pass, both worth avoiding:\n\n- **Include `.lib`.** A `MUST_HAVE` regex of `rac_commons\\.(dylib|so|dll|a)$` silently skips\n  `RACommons-windows-x64/x64/rac_commons.lib` — an **89 MB** binary that then gets no positive\n  check at all. Match `(dylib|so|dll|a|lib)` plus `\\.wasm$`. Correct coverage is **38** binaries,\n  not 37.\n- **Assert per binary, not by set-intersection.** Intersecting \"non-public origins across all\n  files\" yields more than one origin here (`hf.co` *and* the staging host), so a binary carrying\n  only `hf.co` would pass **vacuously**. Check each binary individually for the staging host.\n\n```bash\n# v0.20.25 baseline: 38 commons/wasm/lib binaries; 0 missing the staging host.\n```\n\nNever print the URL — it is a repo secret. Report a truncated SHA-256 fingerprint, or just assert presence/absence counts.\n\n### (7) Routability gate over the whole tree\n\n```bash\npython3 scripts/validation/gates/check_plugin_natives.py \"$W/x\" \"$W/nested\" > /tmp/gate.txt 2>&1\ngrep -cE '^  ok ' /tmp/gate.txt; grep -E '^  FAIL' /tmp/gate.txt\n```\n\nv0.20.25 baseline: **77 ok, 1 FAIL**. The single expected FAIL is\n`RACommons-linux-x86_64/lib/librunanywhere_sherpa.so` — see the known-exception note below. **Any other FAIL blocks the publish.**\n\n### (8) Known exception — Linux sherpa is a non-routable shell (PRE-EXISTING, not a regression)\n\n`RACommons-linux-x86_64-v*.tar.gz` ships **no** `libsherpa-onnx-c-api.so` at all and its `librac_backend_sherpa.so` is a ~156 KB shell (~3 sherpa strings, vs ~31 MB / ~192 for a healthy one on Electron macOS) — the engine is not linked in, so the carrier cannot reference `g_sherpa_{stt,tts,vad}_ops` and **STT/TTS/VAD cannot work from that bundle**. Android's carriers pass on all 3 ABIs, so this is Linux-only.\n\n**Compare the CARRIER, not the backend.** The gate FAILs on `librunanywhere_sherpa.so` (the carrier); citing `librac_backend_sherpa.so` as your evidence is comparing a *different file* and is not proof. The carrier is byte-identical across releases, which is the real proof:\n\n```bash\ngh release download \"v<PREV>\" --repo \"$REPO\" -p 'RACommons-linux-x86_64-v<PREV>.tar.gz' -D /tmp/prev\ntar -xzf /tmp/prev/*.tar.gz -C /tmp/prev\nshasum -a 256 /tmp/prev/lib/librunanywhere_sherpa.so \"$W\"/x/RACommons-linux-*/lib/librunanywhere_sherpa.so\n# v0.20.24 and v0.20.25 both: size=15712  sha256=28ef16015b61da86...\n\n# strongest check — run the gate on the PREVIOUS bundle and confirm the identical FAIL:\npython3 scripts/validation/gates/check_plugin_natives.py /tmp/prev\n```\n\nIf it ever regresses *further* (e.g. Android or macOS sherpa also goes hollow), stop and treat it as a blocking defect.\n\n### (9) Account for every payload explicitly\n\nPrint a per-artifact table (checksum verdict, binary count, placeholder hits) and confirm the row count equals the payload count. Artifacts that legitimately contain **no** native binaries, and why — do not let these pass silently unexamined:\n\n| Artifact | Why it has no binaries |\n|---|---|\n| `RunAnywhereMLXResources-ios-*.zip` | 25 third-party license/notice text files |\n| `runanywhere-kotlin-maven-v*.zip` | binaries live in the **nested** `.aar`s (step 4) |\n| `runanywhere-proto-ts-*.tgz` | pure TS/JS (69 `.ts` + 69 `.js`) |\n\nEverything else must have a non-zero, plausible binary count. A platform suddenly shipping fewer binaries than the previous release is a regression signal worth chasing before publishing.\n\n### (10) Blind spot to be aware of — Windows x64 has NO routability coverage\n\n`RACommons-windows-x64-v*.zip` ships **only** a static `rac_commons.lib` (~89 MB) plus 152 `.h` / 11 `.hpp` headers — **zero backend DLLs**, where Linux/Android/Web each ship 4–5 backend carriers. That is consistent with a static-embedding bundle (consumers link the engines in themselves), but the consequence matters: **no gate can detect a missing or hollow Windows backend**, because there is no carrier to inspect. `check_plugin_natives.py` therefore reports nothing at all for this artifact rather than reporting health.\n\nWindows x64 is also *advisory* in `release.yml` (its absence does not fail the release). So a Windows regression can pass every automated check. If Windows correctness matters for a given release, verify it by actually running something on the real Windows box (see §Electron for how to reach it) — do not infer it from a green pipeline. Worth a deliberate confirmation with the repo owner that the DLL-free shape is intentional.\n\n---\n\n## General verification discipline (every artifact, every registry)\n\nApply all three checks below to **every** file you're about to publish, before AND after the publish call. Never trust a copy cached locally earlier in the session — re-download.\n\n**(a) Download + verify the sha256 sidecar yourself:**\n\n```bash\ngh release download v$VERSION -p '<file>' -p '<file>.sha256' -D /tmp/sdk-publish-$VERSION\ncd /tmp/sdk-publish-$VERSION && shasum -a 256 -c '<file>.sha256'\n```\n\n**(b) Inspect the actual file listing inside every tarball/AAR/zip** — never assume from the filename alone:\n\n```bash\nnpm pack <tgz-or-package-dir> --dry-run     # npm packages: prints the exact file list it would publish\nunzip -l  <file.zip>                        # AARs, Maven repo zip, native archives\ntar -tzf  <file.tar.gz>                     # rcli, RACommons tarballs\n```\n\nSanity-check what comes back:\n- No suspiciously-small or empty binaries where a real native library (`.so`, `.dylib`, `.a`, `.dll`) should be — a stub or a zero-byte placeholder means the build silently failed upstream.\n- No leftover `workspace:*` or `file:` dependency specs in any `package.json` inside the tarball — those only resolve inside this monorepo and break for a real external installer.\n- Version strings inside every manifest (`package.json` `version`, AAR `pom.xml` `<version>`, `pubspec.yaml` `version:`) **exactly** match `$VERSION`.\n- **Hard rule, no exceptions:** grep the full file listing for `qhexrt` (any case) and for QNN binary names (`libQnnHtp`, `QnnHtp*.dll`, `rac_commons.dll` bundled with QHexRT, `runanywhere_qhexrt`). If any hit appears in a package meant to be public, stop and do not publish that artifact.\n\n**(c) Confirm baked-in runtime config** (e.g. a staging-vs-prod backend base URL compiled into `rac_commons` at build time) points at the real intended endpoint. This is designed to fail closed if misconfigured — a build that completed and produces working requests against prod is itself decent evidence — but if you have any doubt, `strings <binary> | grep -i \"api\\.\\|staging\\.\"` and eyeball it.\n\n## 1. npm — 8 packages, order matters\n\nPackages: 4 Web (`@runanywhere/proto-ts`, `@runanywhere/web`, `@runanywhere/web-llamacpp`, `@runanywhere/web-onnx`) + 4 React Native (`@runanywhere/core`, `@runanywhere/llamacpp`, `@runanywhere/mlx`, `@runanywhere/onnx`). Never `@runanywhere/qhexrt` (RN) — it exists in `bindings/react-native/packages/qhexrt/` but is excluded from the public npm train by the same policy as Maven/pub.dev QHexRT.\n\n**Publish `@runanywhere/proto-ts` FIRST.** `@runanywhere/web-onnx` deliberately does not bundle its own `proto-ts` copy and needs it resolvable from the live registry for real consumers; publish `proto-ts` before it or a fresh `npm install @runanywhere/web-onnx` fails to resolve.\n\n```bash\ngh release download v$VERSION -p 'runanywhere-proto-ts-*.tgz*' -p 'runanywhere-web-*.tgz*' \\\n  -p 'runanywhere-core-*.tgz*' -p 'runanywhere-llamacpp-*.tgz*' -p 'runanywhere-mlx-*.tgz*' -p 'runanywhere-onnx-*.tgz*' \\\n  -D /tmp/sdk-publish-$VERSION\ncd /tmp/sdk-publish-$VERSION\nfor f in *.sha256; do shasum -a 256 -c \"$f\"; done\n\nnpm publish runanywhere-proto-ts-$VERSION.tgz --access public   # FIRST, always\nnpm publish runanywhere-web-$VERSION.tgz --access public\nnpm publish runanywhere-web-llamacpp-$VERSION.tgz --access public\nnpm publish runanywhere-web-onnx-$VERSION.tgz --access public\nnpm publish runanywhere-core-$VERSION.tgz --access public\nnpm publish runanywhere-llamacpp-$VERSION.tgz --access public\nnpm publish runanywhere-mlx-$VERSION.tgz --access public\nnpm publish runanywhere-onnx-$VERSION.tgz --access public\n```\n\n`npm whoami` should already show a logged-in account; if it errors, stop and tell the human rather than trying to log in non-interactively.\n\n**GOTCHA — large tarballs can silently die mid-upload.** `@runanywhere/mlx` (~50MB) and `@runanywhere/web-onnx` (unpacks to 300+MB) can exceed a short default shell/tool timeout mid-upload and get killed with no clean error. Use an explicit long timeout (5 minutes / 300000ms) on each publish call.\n\n**GOTCHA — verify via the registry API, not the npm CLI.** `npm view` can lag the CLI's own cache by several seconds right after a fresh publish and falsely report \"not found\". After **every** publish:\n\n```bash\ncurl -s \"https://registry.npmjs.org/@runanywhere/<pkg>\" | python3 -c \"import json,sys;print(json.load(sys.stdin)['dist-tags'])\"\n```\nConfirm `latest` equals `$VERSION`.\n\n## 2. Maven Central — 3 AARs only\n\n`io.github.sanchitmonga22:{runanywhere-sdk,runanywhere-llamacpp,runanywhere-onnx}`. **Never `runanywhere-qhexrt-android`** — it was published through 0.20.19 under an earlier, since-reversed policy and is now permanently excluded; `release.yml`'s manifest assertion hard-fails if it reappears in the public asset set.\n\nFull step-by-step lives in `bindings/kotlin/docs/KOTLIN_MAVEN_CENTRAL_PUBLISHING.md`, section **\"Option A: Publish with released native archives\"** (line ~108 as of this writing — the doc has been corrected mid-release before, so re-read it fresh rather than trusting memory of its contents). Key points, condensed:\n\n1. Download + verify the 3 `RACommons-android-{arm64-v8a,armeabi-v7a,x86_64}-v$VERSION.zip` from the release (sha256 sidecars, per the general discipline above).\n2. Stage: `bash bindings/kotlin/scripts/package-sdk.sh --mode local --natives-from <dir-with-the-3-zips>`. This also produces a local Maven-repo zip — diff its size against `runanywhere-kotlin-maven-v$VERSION.zip` already attached to the GitHub Release as a cheap reproducibility check.\n3. Before uploading, confirm the OSSRH compatibility service has no dangling open repository for this namespace:\n   ```bash\n   CENTRAL_BEARER=\"$(printf '%s:%s' \"$MAVEN_CENTRAL_USERNAME\" \"$MAVEN_CENTRAL_PASSWORD\" | base64 | tr -d '\\r\\n')\"\n   curl -s -H \"Authorization: Bearer $CENTRAL_BEARER\" \\\n     'https://ossrh-staging-api.central.sonatype.com/manual/search/repositories?ip=client&profile_id=io.github.sanchitmonga22'\n   ```\n   Resolve any stale repository per the doc's troubleshooting table before continuing.\n4. Source `MAVEN_CENTRAL_USERNAME`/`MAVEN_CENTRAL_PASSWORD` from `~/.gradle/gradle.properties` (`mavenCentral.username`/`mavenCentral.password` — confirmed present on this machine; never print the values) and run, from `bindings/kotlin/`:\n   ```bash\n   ./gradlew clean \\\n     :publishReleasePublicationToMavenCentralRepository \\\n     :modules:runanywhere-core-llamacpp:publishReleasePublicationToMavenCentralRepository \\\n     :modules:runanywhere-core-onnx:publishReleasePublicationToMavenCentralRepository \\\n     -Prunanywhere.useLocalNatives=true -x buildLocalJniLibs --no-daemon\n   ```\n   GPG signing (`signing.gnupg.keyName`/`signing.gnupg.passphrase` in the same properties file, key id `CC377A9928C7BB18` — confirmed importable via `gpg --list-secret-keys --keyid-format LONG` on this machine) is read from the same properties file automatically; you do not pass it separately.\n5. Transfer to Central Portal:\n   ```bash\n   curl --fail --request POST -H \"Authorization: Bearer $CENTRAL_BEARER\" \\\n     'https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/io.github.sanchitmonga22?publishing_type=automatic'\n   ```\n   Then find the resulting `portal_deployment_id` — expect **exactly one** fresh repository; if the count isn't 1, stop and investigate rather than guessing which one is yours (see the doc for the exact `jq` filter).\n6. Poll `https://central.sonatype.com/api/v1/publisher/status?id=<id>` (POST, same bearer) until `deploymentState` is `PUBLISHED`. Automatic deployments normally go `PENDING -> VALIDATING -> PUBLISHING -> PUBLISHED` within a few minutes. **If it lands on `VALIDATED` instead of proceeding, that means Sonatype is waiting for a manual click in the Central Portal UI** — stop and tell the human; do not loop forever assuming it resolves itself. On `FAILED`, print `.errors` and stop.\n7. Verify propagation (allow 10-30 minutes for CDN propagation even after `PUBLISHED`):\n   ```bash\n   for a in runanywhere-sdk runanywhere-llamacpp runanywhere-onnx; do\n     echo \"$a: $(curl -s -o /dev/null -w '%{http_code}' \"https://repo1.maven.org/maven2/io/github/sanchitmonga22/$a/$VERSION/$a-$VERSION.pom\")\"\n   done\n   ```\n   Expect `200` for all three.\n\n## 3. pub.dev (Flutter)\n\n**Verify the current package set fresh — do not trust a hardcoded count.** As of this writing the real, non-QHexRT Flutter package set is **4**, not 3: `runanywhere`, `runanywhere_llamacpp`, `runanywhere_onnx`, and `runanywhere_mlx` (mlx was added after some earlier documentation/memory of \"3 packages\" was written — that assumption is stale; `runanywhere_mlx` is a real, already-published-on-pub.dev package, not new/WIP). Enumerate it yourself instead of trusting this list verbatim next time:\n```bash\nfind bindings/flutter/packages -maxdepth 1 -type d -not -iname '*qhexrt*' -not -path '*/packages'\n```\n`runanywhere_qhexrt` exists in that directory too — never publish it (same exclusion policy as Maven).\n\nStage the released iOS xcframeworks + Android `.so` into each package with the Flutter equivalent of `package-sdk.sh` (melos-based monorepo, confirmed at `bindings/flutter/scripts/package-sdk.sh`):\n\n```bash\nbash bindings/flutter/scripts/package-sdk.sh --mode local --natives-from <dir-with-xcframeworks-and-android-.so>\n```\n\nThis script already runs `flutter pub publish --dry-run` internally per package as part of its own validation — but run it again yourself, per package, from each package directory, and **read the output for real, not just the exit code**:\n\n```bash\ncd bindings/flutter/packages/runanywhere && flutter pub publish --dry-run\ncd ../runanywhere_llamacpp && flutter pub publish --dry-run\ncd ../runanywhere_onnx && flutter pub publish --dry-run\ncd ../runanywhere_mlx && flutter pub publish --dry-run\n```\n\nExpect zero warnings except a benign version-gap hint. Only then run the real publish, same order (core first, since the others depend on it):\n\n```bash\ncd bindings/flutter/packages/runanywhere && flutter pub publish\ncd ../runanywhere_llamacpp && flutter pub publish\ncd ../runanywhere_onnx && flutter pub publish\ncd ../runanywhere_mlx && flutter pub publish\n```\n\n**Credential note:** pub.dev OAuth is normally cached at `~/Library/Application Support/dart/pub-credentials.json` on macOS (confirmed present on this machine). If it's missing, expired, or `flutter pub publish` prompts for an interactive browser login, **STOP and tell the human** — you cannot complete a fresh Google OAuth flow non-interactively, and forcing/guessing here is not appropriate.\n\nVerify afterward against the live API (not a cached page):\n```bash\nfor p in runanywhere runanywhere_llamacpp runanywhere_onnx runanywhere_mlx; do\n  echo \"$p: $(curl -s \"https://pub.dev/api/packages/$p\" | python3 -c \"import json,sys;print(json.load(sys.stdin)['latest']['version'])\")\"\ndone\n```\n\n## 4. Swift SPM distribution repo (`RunanywhereAI/runanywhere-swift`)\n\nA separate, **generated-only** repo mirroring `bindings/swift` (Package.swift + Sources/ + LICENSE + README) so SwiftPM consumers using `from: \"<version>\"` don't clone the ~340MB monorepo.\n\n**GOTCHA:** this machine may have local directories literally named `runanywhere-swift` that are just checkouts of the **monorepo's** remote, not the distribution repo. Verify before trusting any existing local directory by name:\n```bash\ngit -C <candidate-dir> remote get-url origin\n```\nMust show `RunanywhereAI/runanywhere-swift`, not `runanywhere-sdks`. Always clone fresh instead of guessing:\n\n```bash\ngit clone https://github.com/RunanywhereAI/runanywhere-swift.git /tmp/ra-swift\n\nbindings/swift/scripts/sync-dist-repo.sh \\\n  --zips <exact-verified-Apple-archive-dir-from-the-release> --tag /tmp/ra-swift\n\nRUNANYWHERE_SWIFT_DIST_REPO=/tmp/ra-swift \\\n  bash scripts/validation/gates/check_swift_dist_repo_sync.sh\n\ngit -C /tmp/ra-swift push origin main --follow-tags\n```\n\nThe `--zips` dir must contain the verified (sha256-checked) release archives — `RACommons-ios-v$VERSION.zip`, `RABackend{LLAMACPP,MLX,NeuRT,ONNX,Sherpa}-ios-v$VERSION.zip`, `RunAnywhereMLX{Metal,Resources,Runtime}-ios-v$VERSION.zip` — downloaded and checked exactly per the general verification discipline, not re-used from an earlier, untrusted local copy.\n\n**The tag on `runanywhere-swift` is bare semver, no `v` prefix** (SwiftPM's `from:` needs that) — do not tag it `v$VERSION` by mistake; `sync-dist-repo.sh --tag` handles this correctly, but double check the pushed tag with `git -C /tmp/ra-swift tag --points-at HEAD` before walking away.\n\nThis is enforced, not just documented: once `v$VERSION` is tagged on `runanywhere-sdks`, `check_swift_dist_repo_sync.sh` fails every future PR on the monorepo until `runanywhere-swift` carries the matching bare-semver tag — so don't let this lag behind the other registries.\n\n**GATE GAP — `check_swift_dist_repo_sync.sh` does NOT catch dependency drift, only checksum/version-string drift.** `runanywhere-swift`'s `Package.swift` is manually maintained end to end — `sync-dist-repo.sh` only ever touches `sdkVersion` and the binaryTarget checksums, never the `dependencies:` block (mlx-swift, mlx-swift-lm, mlx-audio-swift, swift-crypto, etc.). If root `runanywhere-sdks/Package.swift` bumps one of those (e.g. a new fork tag), a human has to remember to mirror it into the dist repo by hand, and nothing fails CI if they forget. This exact drift shipped once (2026-08-17): the dist repo pointed `mlx-swift-lm` at unforked `ml-explore/mlx-swift-lm` instead of the `RunanywhereAI` fork, silently breaking every iOS/macOS MLX consumer of that tag until caught by an actual build failure ~40 minutes later. Before trusting a freshly-cut dist-repo tag: diff every `.package(url:` line between root `Package.swift` and the dist repo's `Package.swift` (`grep -n '\\.package(url:' Package.swift` on both sides), and actually build a real product that exercises the dependency you're worried about (e.g. `cd /tmp/ra-swift && swift build --product RunAnywhereMLX`) — don't rely on the sync gate alone. If you catch drift on a tag that's only minutes old with no evidence of external consumption, fixing it in place and force-moving the tag (`git tag -f $VERSION && git push origin $VERSION --force`) is the correct call, not a shortcut — a known-broken immutable tag helps no one.\n\n**If you force-move a tag, SwiftPM/Xcode consumers cache the OLD commit in several places — clearing just `Package.resolved` is not enough.** Verifying your own fix (or having a downstream consumer verify it) can silently keep failing even after the fix is correctly pushed, because: (1) any consumer repo's own tracked `*.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved` pins the exact old resolution and `xcodebuild -resolvePackageDependencies` respects it rather than re-resolving; (2) `~/Library/Caches/org.swift.swiftpm/repositories/<pkg>-<hash>/` is a global shared clone cache keyed by repo, oblivious to a moved tag; (3) `~/Library/org.swift.swiftpm/security/fingerprints/<pkg>-<hash>.json` records the fingerprint SwiftPM saw for that tag and can silently keep using it after a mismatch instead of failing loud. To actually re-resolve after moving a tag: delete the consumer's tracked `Package.resolved`, `rm -rf .build .swiftpm` and any `~/Library/Developer/Xcode/DerivedData/<Scheme>-*`, AND clear the matching entries under both global `org.swift.swiftpm` cache directories for every affected package identity — then resolve fresh.\n\n## 5. macOS rcli notarization\n\nCI (`native_rcli_macos` in `release.yml`) usually already produces a notarized, stapled artifact when Developer ID + notary secrets are configured in repo secrets. **Check the release assets first** before assuming you need to notarize anything yourself locally:\n\n```bash\ngh release view v$VERSION --json assets --jq '.assets[].name' | grep -iE 'rcli-macos|\\.dmg'\n```\nBaseline expectation is `rcli-macos-arm64-v$VERSION.tar.gz` (+ `.sha256`); a `.dmg` only appears when `RCLI_MACOS_FULL_RELEASE=1` was set for that build (see below). If a plain tarball is already present and its contents pass the general verification discipline, there is usually nothing further to do here.\n\n**If you do need to notarize locally**, everything lives in `rcli/scripts/package-rcli.sh` (read its header comment — it documents its own contract in detail):\n\n- One-time setup, check before redoing: `xcrun notarytool history --keychain-profile runanywhere-notary` (confirmed configured on this machine — a fresh submission history returns without error). If missing: `xcrun notarytool store-credentials \"runanywhere-notary\" --apple-id <email> --team-id <team> --password <app-specific-password>`.\n- Codesign identity: `security find-identity -v -p codesigning | grep \"Developer ID Application\"` — confirmed present on this machine as `\"Developer ID Application: RunAnywhere, Inc (<TEAM_ID>)\"`. Grep for it fresh rather than hardcoding the team-id suffix, it can rotate.\n- Invocation contract (env vars, from the script header): `RCLI_CODESIGN_IDENTITY`, `RCLI_MACOS_NOTARIZE=1`, `RCLI_MACOS_FULL_RELEASE=1`, `RCLI_MACOS_SWIFT_BIN_DIR=<output of build-mlx-cli.sh>`. `RCLI_MACOS_FULL_RELEASE=1` is what additionally stages `mlx.metallib` and SwiftPM resource bundles and is what causes a notarized, stapled `.dmg` to be emitted alongside the Homebrew-style tarball. `RCLI_CODESIGN_KEYCHAIN` / `RCLI_NOTARYTOOL_KEYCHAIN` matter only if the identity/profile live in a non-default keychain.\n- Call shape: `rcli/scripts/package-rcli.sh <build-dir> macos-arm64` with the env vars above exported.\n\n## 6. Electron — build AND packaging are now in `release.yml`; publish is still manual\n\n**Reversing the earlier \"deliberately separate\" note**: as of `fix/electron-cicd-staging-url-npu-ane` (2026-08-21), the repo owner asked for Electron to produce ready-to-publish artifacts through the real release train like every other SDK, so `release.yml` now has `native_electron` (invokes `.github/workflows/electron-native-package.yml` as a reusable `workflow_call`) and `sdk_electron` (downloads its `electron-packaged-tarballs` artifact, runs `scripts/release/prepublish_check.py` on every tarball, checksums them, and splits public vs. QHexRT-private). `publish` hard-gates on `sdk_electron` succeeding. `@runanywhere/electron`, `-llamacpp`, `-onnx`, `-sherpa`, `-neurt` land as real, `.sha256`-sidecarred GitHub Release assets, same as the Web/RN npm tarballs. `@runanywhere/electron-qhexrt` still never becomes a public Release asset (policy below) — it's produced and packaged every release, but only reachable as the `sdk-electron-qhexrt-private` workflow artifact on that run, for a human to `npm publish` by hand. Electron is **not** WIP on a branch anymore: the old root `AGENTS.md` \"Active issues\" section describing `smonga/electron_upgrade` as in-progress is gone entirely as of this checkout — `bindings/electron/` is a fully merged, real SDK on `main`. Re-check that section is still absent before trusting this note.\n\n`.github/workflows/electron-native-package.yml` itself is unchanged in what it builds: every backend for real (llamacpp/onnx/sherpa/neurt on their real platforms, qhexrt on a self-hosted Windows ARM64 runner) with the real `STAGING_BASE_URL` baked in, gated by `check_plugin_natives.py` (which fails closed on the staging-URL placeholder — the exact bug that shipped through `0.20.24`). What's new is that it's also invocable via `workflow_call`, and that `native_win_x64_and_package` now tolerates a failed/skipped QHexRT lane (self-hosted, single machine) rather than losing the other five real packages to one runner's outage. **`release.yml` itself still does not `npm publish` anything** — it only produces artifacts (Release assets for the public five, a workflow artifact for QHexRT) — same \"build automated, publish manual\" shape as every other platform. See `thoughts/shared/plans/electron_cicd_staging_url_and_npu_ane_pipeline.md` for the full history.\n\n**PROVEN END TO END on v0.20.25 (2026-08-22)** — the first release where this actually ran. Final state: 65 release assets, all 5 public Electron tarballs present with `.sha256` sidecars, zero `qhexrt`-named assets. Verified by downloading the *published* tarballs and running the repo's own gate against them:\n\n```bash\ngh release download v<version> --repo RunanywhereAI/runanywhere-sdks -p 'runanywhere-electron*.tgz' -D /tmp/verify\npython3 scripts/validation/gates/check_plugin_natives.py --expect-version <version> \\\n    $(for t in /tmp/verify/*.tgz; do printf -- '--tarball %s ' \"$t\"; done)\n# => All 20 check(s) passed: plugin natives are routable and no staging-URL placeholder was found.\n```\n\nThat single command asserts all three things you actually care about, per binary: the real `STAGING_BASE_URL` is baked in (no `YOUR_STAGING_BASE_URL`), `rac_commons` embeds the right version, and every backend is genuinely **routable** (`g_llamacpp_ops`, `g_sherpa_stt_ops`, `g_neurt_llm_ops`, `g_onnx_embeddings_ops`, …) rather than a same-sized shell. **Run it before every Electron `npm publish`** — it is the cheapest defense against exactly the class of defect that shipped `electron-sherpa` 0.20.17's NULL-vtable carrier. Expected shipped layout: entry package = `darwin-arm64` + `win32-x64` + `win32-arm64`; llamacpp/onnx/sherpa = `darwin-arm64` + `win32-x64`; neurt = `darwin-arm64` only; qhexrt = `win32-arm64` only.\n\nWhere to get the QHexRT tarball for its manual publish (it is deliberately NOT a release asset):\n\n```bash\ngh run download <release-run-id> --repo RunanywhereAI/runanywhere-sdks \\\n    --name sdk-electron-qhexrt-private --dir /tmp/qhexrt\n```\n\nIf asked to actually publish an Electron package (still a real, separate, manual step — this skill's general discipline in §0/§General verification still applies in full):\n- Involve the Windows box for real Windows-native verification before publishing anything Windows-targeted. The team keeps a **Windows ARM64 machine reachable over SSH**; its hostname, user, key, and mesh-VPN node name are operator-specific and deliberately not recorded in this repo — read the Windows/ARM64 host entry out of the operator's `~/.ssh/config` and use that alias. If there is no such entry, the box is not configured for you: say so and ask, rather than guessing at a hostname. Check reachability **first** by asking the mesh-VPN client whether the node is up (e.g. `tailscale status`, matching on the host entry's `HostName`) — if it reports `offline, last seen ...`, the box is asleep, which is not a configuration problem: ask the human to wake it, do not debug it as broken. Only then `ssh -o ConnectTimeout=8 \"$WIN_BOX\" \"whoami\"`. That same machine is **also** registered as a GitHub Actions self-hosted runner (labels `[self-hosted, Windows, ARM64, qhexrt]`) running as a Windows service — a queued `native_win_arm64_qhexrt` job on it means the machine is busy; check `gh api repos/RunanywhereAI/runanywhere-sdks/actions/runners` for the matching runner's `busy`/`status` before assuming it's free for manual SSH work, and vice versa.\n- **`@runanywhere/electron-qhexrt` being public on npm is a CONFIRMED, settled policy decision** (repo owner, 2026-08-20) — it contains real Hexagon NPU binaries (`libQnnHtpV81Skel.so`, `QnnHtp*.dll`, `rac_commons.dll`, `runanywhere_qhexrt.dll`) on purpose. The QHexRT-stays-private policy enforced everywhere else in this skill is specifically about the **GitHub-Release-side** asset set (Android etc.), a different channel — not a contradiction. Stop treating this as an unresolved item to flag; only re-raise it if you find evidence the policy changed.\n\n## 7. Final sanity pass\n\nAfter every registry is done, re-view each package's public listing/API and compare against the previous version — a big unexplained drop in file count or package size is a red flag to investigate before calling the release fully published:\n\n```bash\n# npm\nfor p in proto-ts web web-llamacpp web-onnx core llamacpp mlx onnx; do\n  npm pack @runanywhere/$p@$VERSION --dry-run 2>&1 | tail -5\ndone\n\n# Maven — check module metadata / POM sizes are non-trivial, not near-empty\nfor a in runanywhere-sdk runanywhere-llamacpp runanywhere-onnx; do\n  curl -s -o /dev/null -w \"%{size_download} bytes  $a\\n\" \\\n    \"https://repo1.maven.org/maven2/io/github/sanchitmonga22/$a/$VERSION/$a-$VERSION.aar\"\ndone\n\n# pub.dev\nfor p in runanywhere runanywhere_llamacpp runanywhere_onnx runanywhere_mlx; do\n  curl -s \"https://pub.dev/api/packages/$p\" | python3 -c \"import json,sys;d=json.load(sys.stdin);print('$p', d['latest']['version'])\"\ndone\n```\n\nOnly after this pass — every registry shows `$VERSION`, every artifact's contents check out, and nothing regressed in size/file-count versus the previous release — is the release fully published. Do not skip this step just because each individual publish call reported success; a successful upload is not the same claim as \"the right bytes are now live.\"\n","frontmatter":{"name":"sdk-publish","description":"Manually publish an already-tagged, already-released (non-draft GitHub Release) RunAnywhere SDK version to every package registry (npm, Maven Central, pub.dev, Swift SPM dist repo) plus macOS rcli notarization, with heavy verification that nothing fake/stubbed/broken ships."},"isInternal":false,"tokens":10460,"sizeBytes":39036},{"name":"SKILL.md","path":".agents/skills/sdk-release/SKILL.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/.agents/skills/sdk-release/SKILL.md","title":"sdk-release","category":"anthropic-skill","format":"markdown","content":"---\nname: sdk-release\ndescription: Cut a new RunAnywhere SDK release end-to-end in RunanywhereAI/runanywhere-sdks — version bump through a fully green, tagged, drafted-then-published GitHub Release. Stops BEFORE publishing to package registries (npm/Maven/pub.dev/SwiftPM dist) — that is the separate \"sdk-publish\" skill, invoked as the last step here.\n---\n\n# SDK Release (runanywhere-sdks)\n\nRepo: `~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks` (verify with `pwd`/`ls` —\nthis is a workspace subfolder, not the repo root of the whole Qualcomm/ tree, and paths\ndrift; re-check script paths below still exist before trusting them blindly).\n\nThis is a **runbook**, not a description. Follow the steps in order. Every `gh`/`git`\ncommand below is meant to be run close to verbatim — adjust only the version/PR/run-id\nvalues for the release you're actually cutting.\n\n**THIS SKILL HAS BEEN SKIPPED THREE TIMES IN A ROW (v0.20.29, v0.20.30, v0.20.31),\nproducing the IDENTICAL failure each time, DESPITE step 2/3 below documenting the exact\nfix since the v0.20.25/v0.20.29 incidents.** All three times, an agent ran\n`sync-versions.sh`, opened the PR, merged it, and pushed the tag directly — skipping the\n\"dispatch a release candidate BEFORE merging, sync checksums from it, THEN merge\" sequence\nin steps 2-3 — usually because the version bump was one step inside a longer autonomous\nchain (a downstream repo re-pin, a multi-repo release cascade) and felt like \"just a\nversion bump,\" not a release. **A version bump to `core/VERSION` IS a release. If you are\nabout to run `scripts/release/sync-versions.sh` for ANY reason — including re-pinning this\nrepo as a step in some OTHER repo's release — stop and follow this skill from step 1,\nincluding the candidate dispatch. Do not improvise the sequence from memory even if you\ndid it correctly last time; do not skip it because \"this bump is small\" or \"we're in a\nhurry.\"** v0.20.31 additionally caught a second, previously-undetected instance of the\nsame class of bug in the SAME commit: `RunAnywhereMLXRuntime` in the Flutter podspec (which\n`RAC_CHECKSUM_SKIP` exempts from CI verification, so it fails silently for a consumer\ninstead of failing `publish`) was ALSO stale, and had to be independently re-verified\nagainst the same candidate build — see step 3's checksum-location table; check every row\nin it, not just the one CI complained about.\n\nCompanion skill: **sdk-publish** — invoke it at the very end (step 10) for the actual\nnpm / Maven / pub.dev / SwiftPM-distribution publishing. This skill's job stops at a\npublished (non-draft) GitHub Release with correct assets.\n\n---\n\n## 0. Orientation facts (confirm these still hold before relying on them)\n\n- Canonical version file: `core/VERSION` (single line, e.g. `0.20.24`). `core/VERSIONS`\n  also carries a `PROJECT_VERSION=` line that must agree — `sync-versions.sh` updates\n  both, `auto-tag.yml` reads `core/VERSIONS`, `check_release_version_coherence.sh` reads\n  `core/VERSION`. Don't hand-edit either; always go through the script.\n- `scripts/release/sync-versions.sh <version>` — bumps ~20 files (Package.swift,\n  gradle.properties, all package.json/pubspec.yaml, Kotlin/Swift/TS version constants,\n  AGENTS.md). Accepts a leading `v` and strips it.\n- `scripts/validation/gates/check_release_version_coherence.sh` — the gate that enforces\n  \"PR changing VERSION must carry exactly one `release:patch|minor|major` label, and the\n  new version must be exactly base+1 for that bump type.\" Reads `PR_BASE_SHA` and\n  `PR_RELEASE_LABELS_JSON` env vars when run under CI; locally it only checks the\n  canonical-version-format part.\n- `.github/workflows/pr-build.yml` job **`centralization`** is what actually runs the\n  coherence gate in CI (`Release-train version coherence` step), alongside\n  `check_typescript_centralization.sh`, `check_flutter_centralization.sh`,\n  `check_gradle_centralization.sh`, `check_wasm_provenance_contract.sh`,\n  `check_swift_dist_repo_sync.sh`, `check_agents_claude_sync.sh`,\n  `check_no_hardcoded_defaults.sh`. This job's trigger types are\n  `[opened, synchronize, reopened, labeled, unlabeled]` — **adding the release label\n  after opening the PR fires a fresh run**, which is how the gate clears without a new\n  commit (see step 1).\n- `.github/workflows/release.yml` — the build/package/draft-release workflow. Triggers:\n  tag push `v*.*.*` OR `workflow_dispatch` (works on **any branch**, not just tags) with\n  inputs `version` (required), `validate_external_starters` (bool, default false),\n  `publish_from_run_id` (optional), `reuse_native_web_run_id` (optional).\n- `.github/workflows/auto-tag.yml` — fires on `pull_request: closed` to `main` when the\n  merged PR carries a `release:*` label. Verifies the version bump matches the label,\n  pushes the `v<version>` tag, and **explicitly dispatches a fresh `release.yml` run at\n  that tag** (plain `git push` of a tag with `GITHUB_TOKEN` does not auto-trigger\n  workflows, so it calls `gh workflow run release.yml --ref v<version> -f version=<version>`\n  itself). This is the redundant rebuild you must cancel — see step 7.\n- Branch protection on `main`: check\n  `gh api repos/RunanywhereAI/runanywhere-sdks/branches/main/protection` — 2 approvals +\n  code owner review required, but look for a `bypass_pull_request_allowances` (users/teams/apps)\n  list. If your `gh api user --jq .login` is on that list, `gh pr merge --squash --admin`\n  is a legitimate, pre-configured bypass — not a hack. If not on the list, stop and ask a\n  human to approve/merge.\n- Never let any `qhexrt`-named asset reach the public asset set. `release.yml`'s own\n  manifest-assertion step (`find release-flat -maxdepth 1 -type f -iname '*qhexrt*'`)\n  hard-fails the run if one is found — this is enforced, not just policy.\n- **Electron IS part of `release.yml` now** (as of `fix/electron-cicd-staging-url-npu-ane`,\n  2026-08-21) — this reverses the prior \"deliberately separate\" policy noted below by an\n  earlier pass; the repo owner explicitly asked for Electron to produce ready-to-publish\n  artifacts through the real release train like every other SDK. `native_electron`\n  invokes `.github/workflows/electron-native-package.yml` as a **reusable workflow**\n  (`workflow_call`) rather than duplicating its ~400 lines of hard-won native-build logic\n  inline; `sdk_electron` downloads the resulting `electron-packaged-tarballs` artifact,\n  runs `scripts/release/prepublish_check.py` on every tarball, computes checksums, and\n  splits public vs. QHexRT-private before anything reaches `release-flat`. `publish`\n  hard-gates on `sdk_electron` succeeding (same tier as `sdk_kotlin`/`sdk_web`/\n  `sdk_react_native`) but treats `native_electron`'s own result as advisory, because that\n  job's result folds in `native_win_arm64_qhexrt` — a single, personally-owned\n  self-hosted NPU runner whose outage should cost only `@runanywhere/electron-qhexrt`\n  for that release, not the whole SDK train (`native_win_x64_and_package`, the job that\n  actually produces the tarballs, tolerates a failed/skipped QHexRT lane and still packages\n  the other five real artifacts). Like every other SDK here, this still does **not**\n  `npm publish` anything — it only produces the tarballs as GitHub Release assets\n  (public ones) / a workflow artifact (the private QHexRT one) for a human to publish by\n  hand, matching the \"build automated, publish manual\" shape everywhere else.\n  **Electron is no longer WIP on a branch** — the old root `AGENTS.md`/`CLAUDE.md`\n  \"Active issues\" section describing `smonga/electron_upgrade` as in-progress is gone\n  entirely as of this checkout; `bindings/electron/` is a fully merged, real SDK on\n  `main` with its own `AGENTS.md`. Re-check that section is still absent before trusting\n  this note — if it has reappeared, something regressed.\n  See `thoughts/shared/plans/electron_cicd_staging_url_and_npu_ane_pipeline.md` for the\n  full history (this pipeline shipped the placeholder-URL bug three times before\n  `electron-native-package.yml` existed).\n  **QHexRT public/private boundary — CONFIRMED POLICY, not an open flag**: the repo\n  owner explicitly confirmed (2026-08-20) that `@runanywhere/electron-qhexrt` stays\n  public on npm intentionally (it already is, and is a real, working precedent worth\n  studying before touching QHexRT packaging elsewhere) — the QHexRT-stays-private\n  policy enforced everywhere else in this pipeline is specifically about the\n  **GitHub-Release-side** asset set (e.g. Android), a different distribution channel.\n  This is settled; stop re-flagging it as an unresolved contradiction. Only re-raise it\n  if you find evidence the policy has changed.\n\n---\n\n## 0b. NeuRT is a PINNED PREBUILT now — check this before anything else\n\n`native_ios` no longer compiles the private `neurun` repo. It downloads archives that\nrepo publishes, pinned by tag + SHA-256 in `core/VERSIONS`, the same shape as sherpa-onnx:\n\n```\nNEURUN_REPO=RunanywhereAI/neurun      # shared: one repo publishes BOTH engines\nNEURT_RELEASE_TAG=v<version>          # per-engine: a release may carry one lane only\nNEURT_RAC_ABI_VERSION=9\nNEURT_MACOS_ARM64_SHA256=…\nNEURT_IOS_ARM64_SHA256=…\nNEURT_IOS_ARM64_SIMULATOR_SHA256=…\n```\n\n`NEURUN_REPO` is shared because one neurun release carries both engines. The **tags stay\nper-engine** on purpose: that repo can cut a release for one lane only, so a given tag may\ncarry just one engine's assets.\n\n**Run the gate after re-pinning** — it verifies the pins against the release itself, so a\nmistyped or locally-computed value fails there instead of mid-build:\n\n```bash\nNEURUN_TOKEN=\"$(gh auth token)\" bash scripts/validation/gates/check_engine_prebuilt_pins.sh\n```\n\n### Local engine development — there is no source mode\n\nThe engine has no `NEURT_ROOT` path any more; nothing here compiles engine source. To iterate\non NeuRT, build a slice in the neurun checkout and point the engine at it — no release, no\ntag, no pin edit:\n\n```bash\n# in neurun\nNeuRT/tools/scripts/package-rac-dist.sh --sdk-root <this repo> --version 0.0.0-dev \\\n    --slice macos-arm64 --out /tmp/devdist\nmkdir -p /tmp/devslice && tar -xzf /tmp/devdist/neurt-macos-arm64-v0.0.0-dev.tar.gz \\\n    -C /tmp/devslice --strip-components=1\n\n# here\nNEURT_PREBUILT_ROOT=/tmp/devslice cmake -S . -B build/dev -G Ninja \\\n    -DRAC_BUILD_BACKENDS=ON -DRAC_BACKEND_NEURT=ON -DRAC_RUNTIME_COREML=ON\n# expect: Mode: PREBUILT (macos-arm64, ABI 9)\n```\n\n`NEURT_PREBUILT_ROOT` is a **single-slice** root, which is why\n`build-core-xcframework.sh` deliberately ignores it: packaging needs all three slices, so\nhonouring a single-slice override there would let one staged slice satisfy the guard and\nsilently package two non-routable stubs. Use an obviously-not-a-release version like\n`0.0.0-dev` so a dev slice can never be mistaken for a published one.\n\n**Why this changed, and why it matters to you specifically.** `engines/neurt/CMakeLists.txt`\nused to resolve a `NEURT_ROOT` checkout of that private repo and **regex-parse its root\n`CMakeLists.txt`** for a source list. Under that arrangement, commons widened\n`rac_llm_stream_callback_fn` with `tokens_in_delta`, the pinned neurun commit still passed\n4 arguments, and `native_ios` died with *\"too few arguments to function call, expected 5,\nhave 4\"* on **v0.20.15, v0.20.16 and v0.20.17**. Every packaging job needs `native_ios`,\nso all three shipped **iOS-only — 18 assets where a healthy release has 55+**. That is the\nsingle most expensive failure this runbook exists to prevent, and it came from this seam.\n\n**Before opening the release PR**, confirm the pin is current:\n\n```bash\ngrep -A5 '^NEURT_REPO' core/VERSIONS\ngh release view \"$(grep '^NEURT_RELEASE_TAG=' core/VERSIONS | cut -d= -f2)\" \\\n    --repo RunanywhereAI/neurun --json assets --jq '.assets[].name'\n```\n\nExpect six assets — three `neurt-<slice>-v<version>.tar.gz` plus their `.sha256` sidecars.\n\n**If the plugin ABI changed this cycle** (`RAC_PLUGIN_API_VERSION` in\n`core/include/rac/plugin/rac_plugin_entry.h`), the pinned archives are invalid and\n`native_ios` will fail at the download step. Cut a neurun release first — that repo's\n`release_sdk_archives` skill is the runbook — then re-pin both `NEURT_RELEASE_TAG` and\n`NEURT_RAC_ABI_VERSION` here. The downloader refuses to run if `NEURT_RAC_ABI_VERSION`\ndisagrees with the header, so this cannot be half-done.\n\nProve it locally before trusting a tag build:\n\n```bash\nNEURUN_TOKEN=\"$(gh auth token)\" bash scripts/build/download-neurt.sh --all\n```\n\nThat verifies each tarball's SHA-256 **and** that its `RECEIPT.json` names the same ABI\nversion this repo defines. Then check configure prints `Mode: PREBUILT (<slice>, ABI N)`.\n`SOURCE` means a local neurun checkout was picked up (prebuilt is meant to win, so the\ndownload did not land); `SHELL` means the engine is **not routable** and a release built\nnow would ship an engine that registers, claims Apple availability and serves nothing.\n\n`release.yml` treats the downloader's exit **3** (\"no token — degrade to the shell\") as\n**fatal**. That degrade is right for a fork PR and catastrophic for a release.\n\n### The complete engine matrix — who consumes what, and how\n\nThere are only **three fetch points in the entire repo**. Everything else consumes an\nsdks artifact, which is why a single fetch fix can repair five SDKs at once — and why a\nsingle missing fetch can silently break several.\n\n```\nneurun release (private)\n  ├─ native_ios         ──NeuRT x3 slices──►  RABackendNeuRT.xcframework\n  │                                              ├─ Swift  (Package.swift binary target)\n  │                                              ├─ Flutter / React Native (co-vendored\n  │                                              │   in the onnx package's podspec)\n  │                                              └─ rcli-macos (stages it, then GREPS\n  │                                                  `backends --json` for neurt)\n  ├─ native_android     ──QHexRT arm64-v8a──►  one build stages jniLibs into\n  │                                              Kotlin + React Native + Flutter\n  └─ electron           ──NeuRT macos-arm64 AND QHexRT win-arm64──►  its own natives\n```\n\n| target | NeuRT | QHexRT | note |\n|---|---|---|---|\n| Swift (iOS/macOS) | ✅ xcframework | — | NeuRT is Apple-only |\n| Flutter / React Native (Apple) | ✅ xcframework | — | co-vendored in the onnx package |\n| Flutter / React Native (Android) | — | ✅ jniLibs | from the same Android build as Kotlin |\n| Kotlin (Android) | — | ✅ jniLibs | arm64-v8a only; the only ABI with a Hexagon NPU |\n| Electron macOS | ✅ fetches | — | builds its own natives |\n| Electron win-arm64 | — | ✅ fetches | |\n| rcli macOS | ✅ xcframework | — | **best check in the tree** (below) |\n| rcli linux / windows-x64 | — | — | correct: NeuRT is Apple-only, QHexRT is win-arm64 |\n\n**`rcli-macos` is the strongest verification that exists here.** It greps\n`backends --json` for `\"name\":\"neurt\"` — and a non-routable shell *refuses\nregistration*, so that grep proves the engine is **routable**, not merely present. Copy\nthis pattern when adding an engine rather than checking for a file on disk.\n\n**Two silent-failure traps this matrix exists to prevent:**\n\n- **`RAC_BACKEND_<X>=ON` does not mean the engine works.** Without a payload the engine\n  builds as a non-routable shell and registration is refused — no build error. Android\n  shipped that way for months, and `rcli-macos-release` still sets\n  `RAC_BACKEND_NEURT: ON` regardless of whether a payload was fetched.\n- **`copy_if_exists` in `build-core-android.sh` silently no-ops** when a lib was never\n  built. That is the same guarded-copy pattern that shipped Windows with zero engines for\n  several releases. The release now asserts `librunanywhere_qhexrt.so` is really inside\n  the arm64-v8a archive.\n\n`scripts/validation/gates/check_engine_prebuilt_pins.sh` asserts all of the above plus\nthe pins, the ABI, header drift against the receipt, and (with a token) the release\ncontents. Run it before cutting a release; it is also wired into `pr-build` offline.\n\n### QHexRT is pinned the same way — check it too\n\nThe Hexagon-NPU engine crosses the identical boundary, published by the same neurun release:\n\n```\nQHEXRT_RELEASE_TAG=v<version>\nQHEXRT_ARM64_V8A_SHA256=…      QHEXRT_ARM64_V8A_RECEIPT=…\nQHEXRT_WIN_ARM64_SHA256=…      QHEXRT_WIN_ARM64_RECEIPT=…\n```\n\nTwo pins per ABI, not one: the tarball checksum **and** the payload's own build-receipt\nhash. The receipt is the directory name under `engines/qhexrt/prebuilt/versions/`, so\npinning it is what makes the pin describe the bytes rather than trusting the archive's\nown claim about itself.\n\n```bash\nNEURUN_TOKEN=\"$(gh auth token)\" bash scripts/build/download-qhexrt.sh --abi arm64-v8a\nNEURUN_TOKEN=\"$(gh auth token)\" bash scripts/build/download-qhexrt.sh --abi win-arm64\n```\n\n**`current` selects ONE ABI at a time.** Both payloads can sit in `versions/` together, but\na build targets one platform — so re-run the downloader when switching between an Android\nand a Windows build. The downloader ends by running `validate-qhexrt-prebuilt.py` itself,\nso a bad payload fails at the pin rather than mid-build.\n\n**The QHexRT lane in neurun is deliberately non-blocking.** It runs on a single\nself-hosted Snapdragon box (QAIRT is licence-gated and exists on no hosted runner). If\nthat machine is offline, neurun's release publishes the Apple artifacts and **warns** that\nno QHexRT payload was produced — and the SDK simply keeps its previously pinned prebuilt.\nThat is correct behaviour, not a failure: do not block an SDK release on it. Only treat it\nas blocking if this release is specifically meant to ship new NPU kernels.\n\n**Do not hand-stage `engines/qhexrt/prebuilt/`.** That was the old flow, and it is how the\nElectron win-arm64 lane once pinned a receipt staged months earlier and silently shipped\nNPU kernels from an old commit. If the tree looks stale, re-pin and re-download; never\ncopy a payload in by hand.\n\n---\n\n## 1. Decide the version and open the release PR\n\nCheck for an already-open release PR first — don't duplicate work:\n\n```bash\ngh pr list --repo RunanywhereAI/runanywhere-sdks --label release:patch --label release:minor\n```\n\n(gh ORs multiple `--label` flags into a single AND-filtered query per invocation is not\nguaranteed across all gh versions — if unsure, run the two label queries separately:\n`--label release:patch` then `--label release:minor`, plus `--label release:major`.)\n\nPick a patch/minor/major bump based on the changes since the last tag. Then:\n\n```bash\ngit checkout -b release/v<version>\n./scripts/release/sync-versions.sh <version>      # e.g. 0.20.25 (no 'v' needed, either works)\nbash scripts/validation/gates/check_release_version_coherence.sh\n```\n\nThe coherence script may point out generated locks/changelogs that need regenerating\n(e.g. package-lock.json version bumps it expects but didn't find, or a missing CHANGELOG\nheading for the new version). Fix everything it flags, and add **explicit, human-written\nchangelog entries** — the gate requires a heading for the new version to exist; it does\nnot write prose for you.\n\nOpen the PR and **apply exactly one `release:patch` or `release:minor` (or `release:major`)\nlabel** — this is required, not optional. Two failure modes to know:\n\n- If you forget the label and push a commit later, the `centralization` job's\n  `Release-train version coherence` step fails.\n- **Adding the label after the PR is already open does NOT get picked up by the run that\n  already executed.** Because `pr-build.yml`'s `centralization` job triggers on\n  `labeled`/`unlabeled` PR events, applying the label fires a **new** run of the whole\n  job — that new run is what goes green. Don't push an empty commit to \"retrigger\" it;\n  just add/re-add the label (or use `gh pr edit <pr> --add-label release:patch`, and if it\n  was already present, remove + re-add to force the event).\n\n---\n\n## 2. Get REAL native-artifact checksums BEFORE merging — dispatch a release candidate\n\nDo not merge on faith that native builds will succeed. `release.yml`'s\n`workflow_dispatch` trigger works on **any branch**, including your unmerged release\nbranch — use it to build a full release candidate first:\n\n```bash\ngh workflow run release.yml --ref release/v<version> -f version=<version>\n```\n\nGet the run id and watch it:\n\n```bash\ngh run list --repo RunanywhereAI/runanywhere-sdks --workflow=release.yml --limit 5\n```\n\n**Do not block synchronously waiting on it.** Load the `Monitor` tool (or a background\npoll loop) to watch for completion/failure, and keep doing other release prep (changelog\nreview, CodeRabbit thread triage, branch-protection check) in the meantime. Keep\nnotifications to real state changes only — don't ping on every intermediate job\ncompleting, only on the run's overall failure or full completion.\n\n### REAL job durations — measured, not guessed. NEVER cancel a job for being \"slow\".\n\nMeasured on the 0.20.25 train (2026-08-21/22). A release train legitimately takes\n**4-5 hours** wall-clock when `native_web` has to build.\n\n| Job | Real duration | Notes |\n|---|---|---|\n| **`native_web`** (release.yml) | **~3h51m** | The long pole, by far. Its `Vendor ONNX Runtime and Sherpa-ONNX WASM archives` step alone is **~71 minutes** — it BUILDS onnxruntime + sherpa to WASM from source; it is not a git clone. Then 4 more WASM builds: CPU core+llamacpp+onnx ~89m, WebGPU llamacpp ~37m, WebGPU onnx+sherpa ~33m. |\n| `native_ios` | ~60-75 min | XCFrameworks + MLX. No longer compiles neurun — it downloads the pinned NeuRT archives (see §0b), which is seconds, not minutes. |\n| `swift-spm` (pr-build) | **69-85 min** | `Build XCFramework` step is most of it. Measured across 6 consecutive `main` runs (2026-08-21/22): 69, 78, 75, 85, 77, 69. A run sitting at ~78 minutes is **healthy**, not hung — check this row before reaching for cancel. |\n| `kotlin-android` (pr-build) | ~40 min | |\n| `python-windows` (pr-build) | ~40 min | |\n| `electron (windows-2022)` (electron-sdk-ci) | ~40 min | |\n| `rcli-windows` | ~30 min | |\n| `native_electron / native_win_arm64_qhexrt` | ~28 min | Self-hosted; may sit **queued** for a long time first if the single runner is busy. Queued ≠ hung. |\n| `native_electron / native_win_x64_and_package` | ~35-45 min | |\n| `wasm` (pr-build) | ~12 min | **Do NOT use this as a proxy for `native_web`.** Totally different scope. |\n\n**The mistake this table exists to prevent**: `pr-build.yml`'s `wasm` job takes ~12\nminutes, so a ~50-minute `native_web` \"vendor\" step looks hung. It is not. On the 0.20.25\ntrain this misread caused two healthy `native_web` jobs to be cancelled at 2h19m and\n~55m, wasting ~3 hours and two full candidate builds. Before concluding \"hung\", check\nstep-level progress and compare against THIS table:\n\n```bash\ngh run view <run-id> --repo RunanywhereAI/runanywhere-sdks --json jobs \\\n    --jq '.jobs[] | select(.name==\"native_web\") | {startedAt, steps: [.steps[] | select(.status==\"in_progress\") | .name]}'\n```\n\n### `reuse_native_web_run_id` is the correct way to avoid re-paying that ~4 hours\n\nIf a run needs redoing and **nothing under `bindings/web/wasm/`, `engines/`, or `core/`\nchanged** since a run whose `native_web` succeeded, reuse it instead of rebuilding:\n\n```bash\ngit diff --stat <good-run-sha> <current-sha>      # prove the scope first\ngh workflow run release.yml --ref release/<version> \\\n    -f version=<version> -f reuse_native_web_run_id=<good-run-id>\n```\n\n`native_web` then shows `skipped`, and `sdk_web` + `validate_consumer_web` still run and\npass against the reused artifact — which is also your proof the reuse was valid. Track the\nrun id that **originally built** it: that is the id you keep citing (see the chaining note\nin step 7).\n\n---\n\n## 3. Sync checksums once native_ios finishes\n\nPre-tag, `gh release download` will not work (there is no release yet) — pull the run's\nartifact directly:\n\n```bash\ngh run download <candidate-run-id> --repo RunanywhereAI/runanywhere-sdks \\\n    --name native-ios-macos --dir release-artifacts/native-ios-macos\n```\n\nVerify every `.zip` against its `.sha256` sidecar **yourself** before trusting it:\n\n```bash\ncd release-artifacts/native-ios-macos\nfor f in *.zip; do shasum -a 256 -c \"${f}.sha256\" || echo \"MISMATCH: $f\"; done\ncd -\n```\n\nThen sync:\n\n```bash\nbindings/swift/scripts/sync-checksums.sh release-artifacts/native-ios-macos\n```\n\n**KNOWN FACT — do not treat this as a bug**: `RunAnywhereMLXRuntime` (and occasionally\n`RACommons`) are **not byte-reproducible** between rebuilds of identical source. Expect\ntheir checksums to differ from any prior candidate build every single time you rebuild,\neven with zero source changes. Only investigate if a checksum for a framework that\nhistorically IS reproducible (e.g. plain XCFrameworks with no Swift codegen step)\nsuddenly changes.\n\n**Where each checksum actually lives — these are NOT all in one file:**\n\n| Manifest | Carries |\n|---|---|\n| root `Package.swift` | `RACommons` + the 5 backend XCFrameworks (6 checksums total) |\n| nested Flutter `Package.swift` (under `bindings/flutter/`) | same 6 |\n| `bindings/flutter/packages/runanywhere_mlx/ios/runanywhere_mlx.podspec` → `mlx_checksums` hash | `RunAnywhereMLXRuntime`, `RunAnywhereMLXMetal`, `RunAnywhereMLXResources` — **only here**, nowhere else |\n\nAfter `sync-checksums.sh` runs, verify centralization didn't drift and re-run the version\ncoherence gate locally:\n\n```bash\nbash scripts/validation/gates/check_flutter_centralization.sh\nbash scripts/validation/gates/check_release_version_coherence.sh\n```\n\nCommit and push the checksum-sync commit to the release branch:\n\n```bash\ngit add Package.swift bindings/flutter/Package.swift \\\n    bindings/flutter/packages/runanywhere_mlx/ios/runanywhere_mlx.podspec\ngit commit -m \"release: sync v<version> checksums from candidate run <candidate-run-id>\"\ngit push\n```\n\n### THIS STEP IS NOT OPTIONAL — skipping it fails `publish` after the tag already exists\n\n**What happens if you skip it** (observed on the 0.20.25 train): every build and package\njob goes green, `publish` gets all the way past flatten and the asset-manifest assertion,\nthen dies at **`Verify built Apple checksums match tagged package contracts`**:\n\n```\nmismatch: RACommonsBinary\n  Package.swift: 0609dae6...      release zip: 1ae55ae4...\nmismatch: RABackendLlamaCPPBinary\n  Package.swift: 0597442f...      release zip: 7062f7c8...\n>> Done. 8 processed, 0 missing, 4 failed.\nERROR: built Swift archives do not match the immutable tagged manifest\n```\n\nNote `8 processed / 4 failed` is really **2 distinct archives counted twice** (root\nmanifest + Flutter manifest each). On that train `RACommons` and `RABackendLLAMACPP`\ndrifted while ONNX/Sherpa/NeuRT/MLX/MLXMetal/MLXResources all matched — so \"6 of 8 passed\"\nis a normal-looking partial failure, not evidence of something exotic.\n\n**Recovery once the tag already exists** (this is safe and was done on 0.20.25):\n\n1. Confirm nothing was actually published — a failed `publish` dies *before* creating the\n   release, so there is usually nothing to undo:\n   ```bash\n   gh release view v<version> --repo RunanywhereAI/runanywhere-sdks   # expect \"release not found\"\n   ```\n2. Download the **exact artifacts the release will publish** (the candidate run named in\n   `publish_from_run_id`), verify each against its sidecar, and sync on a branch off `main`.\n3. Merge that branch, then **re-point the tag** at the new commit. `git push origin --delete`\n   can hang on flaky networks; the API is more reliable:\n   ```bash\n   gh api -X DELETE repos/RunanywhereAI/runanywhere-sdks/git/refs/tags/v<version>\n   gh api -X POST   repos/RunanywhereAI/runanywhere-sdks/git/refs \\\n       -f ref=\"refs/tags/v<version>\" -f sha=\"<new-main-sha>\"\n   ```\n   Only do this while **no release exists and nothing has been published**. If a release\n   was already published, do NOT move the tag — cut the next patch version instead.\n4. **Creating a tag via the API DOES trigger `release.yml`'s `push` trigger** (unlike a\n   `GITHUB_TOKEN` git-push of a tag, which does not). So immediately after re-tagging you\n   will have TWO runs: a full rebuild from the `push` event, and your own\n   `workflow_dispatch`. Identify by event and cancel the `push` one:\n   ```bash\n   gh run list --repo RunanywhereAI/runanywhere-sdks --workflow=release.yml --limit 3 \\\n       --json databaseId,event,headBranch --jq '.[] | \"\\(.databaseId) \\(.event) \\(.headBranch)\"'\n   gh api -X POST repos/RunanywhereAI/runanywhere-sdks/actions/runs/<push-run-id>/cancel\n   ```\n\nCheapest prevention: run `sync-checksums.sh --check <zip_dir>` against the candidate's\nartifacts and require **`N processed, 0 missing, 0 failed`** before you merge the release\nPR at all.\n\n**Recurred on v0.20.30 and again on v0.20.31** — same `mismatch: RACommonsBinary` /\n`mismatch: RACommons` failure, same root cause (step 2's candidate dispatch skipped\nentirely both times), fixed the same way both times: compute the real checksum from the\nalready-built release-artifact zip (`gh run download <failed-run-id> --name\nnative-ios-macos`), commit it on a fix branch, merge, then move the tag (§10d) to\nre-trigger `release.yml`. On v0.20.31 this ALSO surfaced the `RunAnywhereMLXRuntime`\npodspec entry as independently stale — `RAC_CHECKSUM_SKIP` means `publish` never checks\nit, so it would have shipped broken to Flutter consumers silently. **When fixing this\nfailure after the fact, check every row of the checksum-location table in step 3, not\nonly the archive named in the CI error** — the ones `RAC_CHECKSUM_SKIP` exempts are\nexactly the ones that won't tell you.\n\n---\n\n## 4. Watch for the \"package intentionally doesn't bundle a sibling dep\" bug class\n\nThis exact release hit a real bug in `bindings/web/scripts/package-sdk.sh`:\n`@runanywhere/web-onnx` deliberately does **not** bundle its own `proto-ts` copy (only\n`core`/`llamacpp` do — enforced by `bindings/web/scripts/validate_public_packages.py` and\nmirrored in `bindings/react-native/scripts/validate_public_packages.py`), but the\npackaging script's install-smoke-test (`verify_candidate_install`, around line 165 of\n`package-sdk.sh`) never supplied a resolution target for that unbundled dependency, so\n`npm install` reached out to the real npm registry for a not-yet-published version and\nfailed with `ETARGET`.\n\nFix pattern (already applied, keep it this way): the `onnx` call to\n`verify_candidate_install` passes the just-built local `proto-ts` tarball\n(`$PROTO_ARCHIVE`, built via `npm pack ../proto-ts`) as an explicit extra install arg —\nsee lines ~209-233 of `bindings/web/scripts/package-sdk.sh`, where `core` and `llamacpp`\ncalls omit it (they bundle proto-ts themselves) but `onnx`'s call appends `\"$PROTO_ARCHIVE\"`.\n\n**Generalize this as a standing check for every release**: grep for the asymmetric\nbundling flags before trusting any packaging script —\n\n```bash\ngrep -n \"expects_bundled_proto\\|--bundle\" \\\n    bindings/web/scripts/package-sdk.sh bindings/react-native/scripts/package-sdk.sh\n```\n\nWhenever a package intentionally does NOT bundle a sibling first-party dependency, its\nlocal pre-publish verification step must supply that dependency some other way (a local\ntarball) — the real registry version genuinely does not exist yet at candidate-build\ntime, and letting `npm`/`yarn` hit the real registry will `ETARGET`.\n\n**Before trusting any packaging script is bug-free, actually run it** against the real\ndownloaded native artifacts and check the exit code — do not just read the code and\nassume it's fine:\n\n```bash\ncd bindings/web && ./scripts/package-sdk.sh   # then check $?\ncd bindings/react-native && ./scripts/package-sdk.sh\n```\n\n### 4b. The two Electron-specific release bugs (both fixed — know the shapes)\n\nThe 0.20.25 train was the **first** release to run Electron packaging inside `release.yml`,\nand it surfaced two defects that no local test or green PR check could have caught. Both\nare fixed; recognize the shapes because they generalize.\n\n**(1) `sync-versions.sh` has a hardcoded package list, and new packages get missed.**\n`bindings/electron/packages/neurt/package.json` sat at the previous version while every\nsibling moved, because `neurt` was added (electron#734) after that list was last touched.\nSymptom: `native_win_x64_and_package` fails with\n\n```\nERROR: npm pack did not produce .../runanywhere-electron-neurt-<version>.tgz\n```\n\n— because `npm pack` correctly produced `...-neurt-<OLD-version>.tgz` from the stale\nmanifest. **Whenever you add a package anywhere, add it to `sync-versions.sh` the same\nday.** Cheap pre-flight check that catches the whole class:\n\n```bash\ngrep -h '\"version\"' bindings/electron/package.json bindings/electron/packages/*/package.json \\\n    bindings/electron/native/package.json | sort -u     # expect exactly ONE distinct version\n```\n\n**(2) `publish` downloads EVERY artifact, including intermediate hand-off bundles.**\n`publish`'s `Download all artifacts` step has no name filter. `electron-packaged-tarballs`\nis `native_win_x64_and_package`'s raw output whose only purpose is feeding `sdk_electron`,\nwhich re-uploads the curated public set as `sdk-electron`. Flattening both duplicated all\n5 Electron tarballs *and* dragged in the raw bundle's own `proto-ts` copy, which collided\nwith `sdk_web`'s:\n\n```\n::error::flatten collision: release-flat/runanywhere-proto-ts-<v>.tgz already exists\n::error::Refusing to publish — 6 filename collision(s) detected during flatten\n```\n\nFixed by pruning that directory in the flatten `find`\n(`-type d -name \"electron-packaged-tarballs\" -prune -o -type f \\( ... \\)`). **Standing\nrule: any new intermediate artifact must be either excluded from the flatten or named so\nit cannot collide.** When editing that `find`, test the exact expression against a fake\n`release-artifacts/` tree locally first — `-prune -o` + `-print0` is easy to get subtly\nwrong and the only other way to find out costs a full release run.\n\n---\n\n## 5. Resolve CodeRabbit (or any bot) review findings properly\n\nDo not resolve a review thread without a real, evidence-backed reply. Steps:\n\n1. Fetch unresolved threads via GraphQL:\n\n```bash\ngh api graphql -f query='\nquery($owner:String!, $repo:String!, $pr:Int!) {\n  repository(owner:$owner, name:$repo) {\n    pullRequest(number:$pr) {\n      reviewThreads(first:100) {\n        nodes { id isResolved comments(first:10) { nodes { id databaseId body } } }\n      }\n    }\n  }\n}' -f owner=RunanywhereAI -f repo=runanywhere-sdks -F pr=<pr-number>\n```\n\n2. For each unresolved thread, reply via REST with `in_reply_to` set to the comment's\n   `databaseId` (not the GraphQL node id), citing concrete evidence — a real run id,\n   a real checksum, a real commit SHA, not \"looks fine\":\n\n```bash\ngh api -X POST repos/RunanywhereAI/runanywhere-sdks/pulls/<pr-number>/comments \\\n    -f body=\"Verified against candidate run <run-id>: RunAnywhereMLXRuntime checksum\nmatches release-artifacts/native-ios-macos/RunAnywhereMLXRuntime-ios-v<version>.zip.sha256.\nSee commit <sha>.\" \\\n    -F in_reply_to=<comment-database-id>\n```\n\n3. Only then resolve, via GraphQL mutation on the thread id from step 1:\n\n```bash\ngh api graphql -f query='\nmutation($threadId:ID!) { resolveReviewThread(input:{threadId:$threadId}) { thread { isResolved } } }\n' -f threadId=<thread-node-id>\n```\n\n---\n\n## 6. Merge\n\nBefore merging, verify:\n\n```bash\ngh pr view <pr-number> --repo RunanywhereAI/runanywhere-sdks --json mergeable,reviewDecision\n```\n\nZero unresolved review threads (re-check the GraphQL query from step 5 — it should return\nnone with `isResolved: false`), and `mergeable == \"MERGEABLE\"`.\n\nConfirm bypass eligibility before relying on `--admin`:\n\n```bash\ngh api repos/RunanywhereAI/runanywhere-sdks/branches/main/protection \\\n    --jq '.required_pull_request_reviews.bypass_pull_request_allowances'\ngh api user --jq .login\n```\n\nIf your login appears in that allowance list (users, or a team/app you belong to):\n\n```bash\ngh pr merge <pr-number> --repo RunanywhereAI/runanywhere-sdks --squash --admin\n```\n\nIf not on the list, **stop** — ask a human to approve or merge instead of forcing it.\n`--admin` is a legitimate configured bypass only for allow-listed identities; do not use\nit to route around review for anyone else.\n\n---\n\n## 7. GOTCHA — cancel the auto-tag-dispatched rebuild, dispatch a publish-only run instead\n\nThe moment the PR merges, `auto-tag.yml` runs, pushes the `v<version>` tag, and **itself**\ndispatches a brand-new full `release.yml` run at that tag (see orientation section — plain\ntag pushes via `GITHUB_TOKEN` don't auto-trigger, so the workflow does it explicitly).\nThis is redundant — you already have a fully green, checksum-verified candidate from step\n2-3 — and wasteful, because Apple/MLX bytes will drift again for zero benefit (see the\nnon-reproducibility fact in step 3).\n\n**Cancel it immediately, before it burns real compute** (ideally within the first minute\nor two, before native_ios starts real work):\n\n```bash\ngh run list --repo RunanywhereAI/runanywhere-sdks --workflow=release.yml --limit 5\ngh api -X POST repos/RunanywhereAI/runanywhere-sdks/actions/runs/<auto-triggered-run-id>/cancel\n```\n\nThen dispatch a **publish-only** run that reuses your already-verified candidate's exact\nbytes instead of rebuilding:\n\n```bash\ngh workflow run release.yml --ref v<version> \\\n    -f version=<version> \\\n    -f publish_from_run_id=<candidate-run-id>\n```\n\n**CRITICAL SUB-GOTCHA**: if your candidate run in step 2 itself passed\n`reuse_native_web_run_id=<X>` to skip rebuilding WASM (a packaging-only re-release), that\ncandidate run **never uploaded its own copy of the `native-web` artifact** — it was\nskipped. `publish_from_run_id` alone will then fail during the release-manifest assertion\nwith `\"Missing or empty Web WASM payload\"`, because the publish job's artifact-download\nstep only pulls `reuse_native_web_run_id`'s artifact when that input is passed to *this*\ndispatch too (see `.github/workflows/release.yml` around the \"Download reused Web natives\"\nstep — it is gated on `inputs.reuse_native_web_run_id != ''`, independent of\n`publish_from_run_id`). You must **also** pass the same original run id:\n\n```bash\ngh workflow run release.yml --ref v<version> \\\n    -f version=<version> \\\n    -f publish_from_run_id=<candidate-run-id> \\\n    -f reuse_native_web_run_id=<X>\n```\n\nThis chains forward across releases: whichever run **originally** built `native_web` is\nthe run you keep citing at every subsequent `reuse_native_web_run_id`, not the most recent\nrun that merely borrowed it from an earlier one. Track that original run id explicitly if\nyou're doing back-to-back packaging-only releases.\n\n---\n\n## 8. Verify the draft release's asset set\n\nThe publish job only creates a **draft** GitHub Release (`softprops/action-gh-release`,\n`draft: true`) with built assets attached — it does not touch npm/Maven/pub.dev at all\n(grep `release.yml` for `npm publish`, `mavenCentral`, `pub publish` — none exist there).\n\nExpected asset count for a full, non-Windows-focused release: **~65 assets** —\n\n- 9 Apple archives (iOS/macOS XCFrameworks: RACommons + 5 backends + MLX runtime/Metal/\n  resources + CoreML/NeuRT)\n- 3 Android RACommons zips (arm64-v8a, armeabi-v7a, x86_64)\n- 1 Linux RACommons tar.gz\n- 1 Windows RACommons zip (preserved as an asset, advisory — see step 9)\n- 1 Web WASM tar.gz (`RACommons-web-v<version>.tar.gz`)\n- 3 rcli tarballs (macOS arm64, Linux x86_64, Windows x86_64 — macOS may also have a\n  notarized `.dmg` if Developer ID/notary secrets are configured; this is soft-skipped\n  otherwise, not a failure)\n- 1 Kotlin Maven repo zip\n- 8 npm tarballs (proto-ts, web core, web-llamacpp, web-onnx, RN core, RN llamacpp, RN mlx,\n  RN onnx)\n- 5 Electron npm tarballs (`runanywhere-electron-<v>.tgz` core, `-llamacpp`, `-onnx`,\n  `-sherpa`, `-neurt`) — added by `sdk_electron`; each has its own `.sha256` sidecar\n  counted separately below. **`electron-qhexrt` is deliberately excluded** — it still\n  gets built and packaged, but only as the `sdk-electron-qhexrt-private` workflow\n  artifact (not a release asset); download it separately for a manual `npm publish`.\n- 1 `MANIFEST.txt`\n- **explicitly ZERO** `qhexrt`-named files (the manifest assertion step hard-fails the run\n  if it finds any — confirm the run actually reached the publish job and didn't die there)\n\nEvery asset above except `MANIFEST.txt` has a `.sha256` sidecar, which is itself a\nseparate asset. **v0.20.25 landed at exactly 65 assets** — use that as the concrete\nbaseline rather than re-deriving the arithmetic.\n\nCross-check every checksum's `.sha256` sidecar against what got committed to\n`Package.swift` / the flutter `Package.swift` / the MLX podspec in step 3 — they must\nmatch byte for byte, because this run's artifacts are exactly what step 10 (sdk-publish)\nships to real consumers.\n\n```bash\ngh run view <publish-run-id> --repo RunanywhereAI/runanywhere-sdks --json jobs \\\n    --jq '.jobs[] | {name, conclusion}'\n\n# Asset count + the Electron set + the qhexrt-leak assertion, in three commands:\ngh release view v<version> --repo RunanywhereAI/runanywhere-sdks --json assets --jq '.assets|length'\ngh release view v<version> --repo RunanywhereAI/runanywhere-sdks --json assets \\\n    --jq '[.assets[].name | select(startswith(\"runanywhere-electron\"))] | sort'\ngh release view v<version> --repo RunanywhereAI/runanywhere-sdks --json assets \\\n    --jq '[.assets[].name | select(test(\"qhexrt\";\"i\"))]'          # MUST be []\n```\n\n### Prove the artifacts are real — don't trust green checkmarks\n\nA green `publish` only proves the assertions ran. To prove the bytes are right, run the\nrepo's own fail-closed gate against the **published** tarballs. This is the single highest\n-value verification in the whole runbook and it takes about a minute:\n\n```bash\ngh release download v<version> --repo RunanywhereAI/runanywhere-sdks \\\n    -p 'runanywhere-electron*.tgz' -D /tmp/verify\npython3 scripts/validation/gates/check_plugin_natives.py --expect-version <version> \\\n    $(for t in /tmp/verify/*.tgz; do printf -- '--tarball %s ' \"$t\"; done)\n```\n\nExpect `All N check(s) passed: plugin natives are routable and no staging-URL placeholder\nwas found.` On v0.20.25 that was **20 checks** across 5 tarballs, asserting three separate\nthings per binary: the real `STAGING_BASE_URL` is baked in (no `YOUR_STAGING_BASE_URL`\nplaceholder), `rac_commons` embeds the right version string, and each backend is actually\n**routable** (real op tables — `g_llamacpp_ops`, `g_sherpa_stt_ops`, `g_neurt_llm_ops`,\netc.), which is what distinguishes a real plugin from a same-sized shell.\n\nSanity-check the shipped layout too — a tarball missing a platform is a silent regression:\n\n| Package | Expected `prebuilds/` |\n|---|---|\n| `@runanywhere/electron` | `darwin-arm64` + `win32-x64` + `win32-arm64` |\n| `-llamacpp`, `-onnx`, `-sherpa` | `darwin-arm64` + `win32-x64` |\n| `-neurt` | `darwin-arm64` only (Apple Neural Engine) |\n| `-qhexrt` (private artifact) | `win32-arm64` only (Hexagon NPU) |\n\n---\n\n## 9. Windows handling — build/preserve only, never auto-publish\n\n`native_windows` and `native_rcli_windows` run and their outputs are **preserved** as\nrelease assets (proving the binaries build), but nothing Windows-specific is published to\nany package registry as part of the standard flow unless a human explicitly asks for it —\nthat's a deliberate, separate decision, not an oversight.\n\nFor anything that needs actual execution on Windows (smoke-testing the native Windows\nbuild, or Electron's Windows packaging), do not assume a macOS cross-build proves\nWindows correctness — SSH to the real Windows box. The team keeps a **Windows ARM64\nmachine reachable over SSH**; its hostname, user, key, and mesh-VPN node name are\noperator-specific and deliberately not recorded in this repo — read the Windows/ARM64\nhost entry out of the operator's `~/.ssh/config` and use that alias. If there is no such\nentry, the box is not configured for you: say so and ask, rather than guessing.\n\n```bash\nWIN_BOX=<the ssh alias from ~/.ssh/config>\n# Ask the mesh-VPN client whether the node is up FIRST (e.g. `tailscale status`,\n# matching on that host entry's HostName) — never just try SSH and retry on timeout.\nssh -o ConnectTimeout=8 \"$WIN_BOX\" \"whoami\"\n```\n\nIf the VPN reports the node offline/asleep, **that's not a config problem** — ask the\nuser to wake the box; don't debug SSH config against a machine that's simply off.\n\n---\n\n## 10. Publish the draft, then hand off\n\nOnce the draft's asset set is verified complete and correct (step 8):\n\n```bash\ngh release edit v<version> --repo RunanywhereAI/runanywhere-sdks --draft=false --latest\n```\n\nThis is the point where SwiftPM's `from: \"<version>\"` resolution and the download URLs\nbaked into `Package.swift` start actually working for real external consumers — draft\nrelease assets are not publicly downloadable, so nothing before this line is\nexternally live.\n\n**Hand off to the `sdk-publish` skill** for the actual npm / Maven Central / pub.dev /\nSwiftPM-distribution-repo publishing steps. This skill's job ends here.\n\n---\n\n## 10b. MANDATORY, and it turns `main` RED until you do it — cut runanywhere-swift\n\nThe moment `v<version>` exists in this repo, `check_swift_dist_repo_sync.sh` (run by\n`pr-build.yml`'s `centralization` job) **fails on every PR and every push to `main`**:\n\n```\n[FAIL] runanywhere-swift has no 0.20.25 tag (latest: 0.20.24)\n       v0.20.25 is published here, so 'from: \"0.20.25\"' is broken for every\n       SwiftPM consumer of https://github.com/RunanywhereAI/runanywhere-swift.git\n```\n\nThis is **by design**, not a bug, and it is the #1 reason a release looks \"not green\"\nafter everything else succeeded. Do not go hunting for a code defect — cut the repo:\n\n```bash\ngit clone https://github.com/RunanywhereAI/runanywhere-swift.git /tmp/ra-swift\nbindings/swift/scripts/sync-dist-repo.sh --zips <zip_dir> --tag /tmp/ra-swift\n# verify BEFORE pushing (content check passes locally; the tag check needs the push)\nRUNANYWHERE_SWIFT_DIST_REPO=/tmp/ra-swift \\\n    bash scripts/validation/gates/check_swift_dist_repo_sync.sh\ngit -C /tmp/ra-swift push origin main --follow-tags\nbash scripts/validation/gates/check_swift_dist_repo_sync.sh    # now [OK]\n```\n\nGotchas found doing this on 0.20.25:\n\n- `--check` **cannot be combined** with `--zips`/`--commit`/`--tag` (hard error). Run a\n  bare `--check <repo>` first to preview, then the real invocation separately.\n- `--check` legitimately reports `ERROR: distribution repo is out of sync` before you\n  sync — that's the preview telling you what it will change (expect the `sdkVersion` bump,\n  ~3 changed files, `0 added / 0 removed`). Non-zero exit there is not a failure.\n- The script **never pushes**; `--tag` only creates the tag locally. The dist repo tag has\n  **no `v` prefix** (`0.20.25`, not `v0.20.25`) while the monorepo tag does.\n- Running the gate locally right after `--tag` still shows `[FAIL] ... has no <version>\n  tag` while simultaneously showing `[OK] ... checkout matches: all 6 checksums`. That is\n  expected: the content check reads your local checkout, the tag check queries the real\n  remote. It clears the moment you push.\n- It chains into `sync-checksums.sh`, so run it **after** step 3's checksums are committed,\n  or the dist manifest gets stale hashes.\n\n---\n\n## 10c. Windows + self-hosted-runner traps (read before debugging a red Windows job)\n\nThese all cost a full CI cycle. The unifying fact:\n\n> The self-hosted runner service executes as `NT AUTHORITY\\NETWORK SERVICE`, which has its\n> own PATH **and** its own NTFS permissions. Anything you verify over SSH on that box tells\n> you nothing about what the job can do. `native_win_arm64_qhexrt` already carries a long\n> comment about this — read it before inventing a fix, and keep it in sync with neurun's\n> equivalent lane, which runs on the *same machine*.\n\n**Environment**\n\n- **`bash: command not found`** — Git for Windows puts `Git\\cmd` (git.exe) on PATH but not\n  `Git\\bin` (bash.exe). `actions/checkout` still works because it calls git.exe directly,\n  which makes this look unrelated to runner setup.\n- **`pwsh` does not exist on that box** — Windows PowerShell 5.1 only. Use\n  `shell: powershell`.\n- **`gh` is not installed on that box, and must not be assumed anywhere.** A QHexRT\n  download died on `gh: command not found`. The fix is deliberately *not* to install it:\n  every fetch of a private release asset now goes through\n  `fetch_release_asset()` in `scripts/build/_release_asset.sh`, which uses `curl` + the\n  REST API. `curl` ships with Windows, macOS and every Linux image we use, so this is one\n  code path everywhere instead of a per-runner provisioning step. If you add a new\n  consumer of a private asset, source that helper — do not reach for `gh`.\n  Note the helper matches the asset name **exactly**: a prefix match picks the `.sha256`\n  sidecar (its name starts with the tarball's), and you get a 65-byte \"archive\" that fails\n  much later with a confusing error.\n- **Tool availability is the single most common failure class on this runner** — `bash`,\n  `python3`, `ninja`, `gh`, GNU `tar` and NDK `.exe` suffixes have each broken a lane\n  once. When a self-hosted job fails, check \"does this tool exist as spelled\" before\n  reading any logic.\n- **`ln -s` under Git-Bash COPIES the directory** unless `MSYS=winsymlinks:nativestrict`.\n  It does not fail, so the result looks like real content and whatever validates a\n  \"must be a symlink\" invariant rejects a payload that was actually fine. Never use shell\n  `ln -s` on this box — go through `scripts/build/_selection.py`.\n- **Windows selects with a JUNCTION, not a symlink, and that is deliberate.** The runner\n  service account cannot create symlinks (below), but a junction is the same kind of\n  reparse point to a directory and needs no privilege. Two traps when reading that code:\n  `Path.is_symlink()` returns **False** for a junction (`stat.S_ISLNK` is true only for\n  `IO_REPARSE_TAG_SYMLINK`) — use `_selection.is_selection()`; and a junction stores an\n  **absolute** t","frontmatter":{"name":"sdk-release","description":"Cut a new RunAnywhere SDK release end-to-end in RunanywhereAI/runanywhere-sdks — version bump through a fully green, tagged, drafted-then-published GitHub Release. Stops BEFORE publishing to package registries (npm/Maven/pub.dev/SwiftPM dist) — that is the separate \"sdk-publish\" skill, invoked as the last step here."},"isInternal":false,"tokens":13119,"sizeBytes":50350},{"name":"SKILL.md","path":".agents/skills/sdk-test-starters/SKILL.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/.agents/skills/sdk-test-starters/SKILL.md","title":"sdk-test-starters","category":"anthropic-skill","format":"markdown","content":"---\nname: sdk-test-starters\ndescription: Run a REAL end-to-end inference smoke test (model download + load + generate, with visible output) on every RunAnywhere starter app — iOS, macOS/Swift, Android, Web, Electron — after a runanywhere-sdks release. Build success alone is never sufficient evidence.\n---\n# SDK Test Starters\n\nPost-release validation runbook for the five RunAnywhere SDK consumer apps. This\nskill exists because \"it built\" has been reported as \"tested\" before, and that is\nnot evidence of anything except that the compiler ran.\n\n## 0. The evidence bar (applies to every app, no exceptions)\n\nFor each app you must produce, and be able to state explicitly:\n\n1. **Version proof** — print/log the actual resolved SDK version from inside the\n   running app (a startup log line, a version screen, an `swift package show-dependencies`\n   / `./gradlew dependencies` / `npm ls @runanywhere/...` output, etc). Never assume\n   a version bump \"took\" just because you edited a manifest.\n2. **Real model acquired** — a real (small) model downloaded fresh, or confirmed\n   already cached, by the app itself (not a file you hand-placed unless that IS the\n   app's supported flow).\n3. **Load** — the model actually loads (watch for a load-succeeded log/state, not\n   just \"no crash\").\n4. **One real inference call** — LLM `generate()` is almost always the easiest\n   modality to drive; use it unless another modality is genuinely less work for\n   that specific app.\n5. **Visible output** — capture the actual generated text (or audio/image) and\n   quote it in your report. \"It responded\" is not enough — show what it said.\n6. **Logs reviewed** — read the console/log stream for errors and warnings and\n   explicitly explain any that appear. Do not silently drop a stack trace because\n   the app \"still produced output.\"\n\nReporting an app as tested from a build/compile success alone is a skill violation.\nIf you could not get past build, that is a BLOCKED report (see §8), not a PASS.\n\n## 1. Locating each app's real local checkout\n\nThis machine accumulates many differently-named copies of the same repos over time\n(worktrees, experiment clones, old branches, scratch dirs). **Never trust a\ndirectory name.** Before touching anything in a candidate directory:\n\n```bash\ngit remote get-url origin          # must be https://github.com/RunanywhereAI/<repo>.git\ngit status --short --branch        # what branch, what's dirty, is it someone else's WIP?\n```\n\nAs of this writing, the known-good local checkouts for this user are here (verify\nthis each time — do not hardcode it further than \"check this location first\"):\n\n```\n~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-ios       -> RunanywhereAI/runanywhere-ios.git\n~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-android   -> RunanywhereAI/runanywhere-android.git\n~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-web       -> RunanywhereAI/runanywhere-web.git\n~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-electron  -> RunanywhereAI/runanywhere-electron.git\n```\n\nThe macOS/Swift case has no separate consumer repo — see §4, it lives inside\n`~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks` (confirm with\n`git remote get-url origin` there too — this machine also has stray full-monorepo\nclones elsewhere, e.g. under `~/development/hard/ambient/`, that are NOT the one to\nuse unless you've verified it's the intended checkout).\n\nThese starter checkouts are commonly sitting on a feature branch, not `main` — e.g.\nat last check `runanywhere-ios` and `runanywhere-android` were on\n`add-gemma4-qwen3.6-qwen3.8-models` and `runanywhere-web` on `add-supertonic-3-tts`.\nThat is fine to test FROM, but say so in your report, and do not `git checkout` away\nfrom a branch that has uncommitted work you didn't create — if `git status --short`\nshows dirty files you don't recognize, stop and ask rather than stashing/resetting.\n\nIf genuinely unsure which directory is real, `git clone` a fresh one into the\nscratchpad rather than guessing — a wrong guess wastes far more time than a clone.\n\n## 2. Cross-repo facts this skill assumes\n\n- SDK version SoT: `core/VERSION` in `runanywhere-sdks`, bumped everywhere by\n  `scripts/release/sync-versions.sh <version>`.\n- Release publish is `.github/workflows/release.yml` (dispatch or `v*.*.*` tag).\n- **Never** treat a build success against `RUNANYWHERE_USE_LOCAL_NATIVES=1` (or any\n  monorepo-local staged artifact) as proof the *public* release works — see §4.\n- QHexRT/QNN material must never appear in a public artifact — release.yml\n  hard-fails on it. If anything you're testing surfaces a QHexRT asset in a public\n  package, stop and flag it, don't route around it.\n\n## 3. iOS — `runanywhere-ios`\n\nSDK pin lives in `Package.swift`:\n\n```swift\n.package(url: \"https://github.com/RunanywhereAI/runanywhere-swift.git\", from: \"0.20.19\")\n```\n\nThis only resolves once `runanywhere-swift` (the generated Swift-only SPM\ndistribution repo — see `~/.claude/skills/sdk-publish/SKILL.md` §4) is\nactually tagged at the target version. Steps:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-ios\n# bump the `from:` version in Package.swift to the target release, then:\nswift package update            # or open in Xcode and let it resolve\n./scripts/build_and_run_ios_sample.sh simulator \"iPhone 16 Pro\"\n```\n\n`./scripts/verify.sh` is the CI-equivalent gate: it resolves the package remotely\nand runs a full `xcodebuild` against the iOS Simulator — use it to prove the\nresolve is clean (it fails if `Package.resolved` ends up dirty, i.e. the committed\npin was stale). `./scripts/smoke.sh` is a fast, non-compiling grep-based check —\nit does NOT count as evidence for this skill's bar, only `build_and_run` +\nwatching real output does.\n\nDrive one real `generate()` call through the app UI (or the equivalent debug\naction if the sample exposes one) against a small downloaded model, then watch the\nruntime log stream for the version line, the model download/load, the generation,\nand any errors:\n\n```bash\nlog stream --predicate 'subsystem CONTAINS \"com.runanywhere\"' --info --debug\n```\n\nMost loggers use `com.runanywhere.RunAnywhereAI`; a few use plain\n`com.runanywhere`; the SDK logs under its own subsystem — the `CONTAINS` match\ncovers all of them. On a physical device use `idevicesyslog | grep \"com.runanywhere\"`\ninstead.\n\n## 4. macOS/Swift — no separate repo, use the in-tree example or rcli\n\nThere is no `runanywhere-macos` consumer repo. Two in-monorepo options, both inside\n`~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks`:\n\n- `bindings/swift/example/` — a minimal SwiftPM example app/executable.\n- `rcli/` — the C++ core's CLI, `rac_*` C ABI consumer.\n\n**The critical gotcha**: the repo's own `Package.swift` is fail-closed toward the\n*real published release* — it resolves the remote `binaryTargets` unless you\nexplicitly opt into locally staged XCFrameworks with\n`RUNANYWHERE_USE_LOCAL_NATIVES=1`. Every doc/script example in this repo sets that\nenv var (because local dev wants fast iteration against in-progress binaries) —\nbut that means copy-pasting those examples verbatim tests your LOCAL staged build,\nnot the actual public release artifact. To prove the published release works:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks/bindings/swift/example\n# DO NOT set RUNANYWHERE_USE_LOCAL_NATIVES — leave it unset so Package.swift\n# resolves the real remote release binaries for the target version.\nswift build\nswift run runanywhere-minimal \"Name three colours.\"\n```\n\nIf the example was previously built/run with the env var set, `swift build` may\nreuse a stale resolve — run `swift package reset` (or delete `.build/`) first if\nyou're not sure which mode the last resolve used.\n\nFor rcli instead (also validates the real published core):\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks\nrcli models download qwen3          # ~639MB, small enough for a quick smoke test\nrcli llm generate --model qwen3 \"Reply with exactly: RCLI WORKS\" --reasoning off\n```\n\n`rcli models list --all` shows the rest of the built-in catalog\n(`whisper-tiny`, `smolvlm2`, `piper`, …) if LLM generate isn't the right modality\nfor what you're verifying.\n\n## 5. Android — `runanywhere-android`\n\nVersion pin: `gradle/libs.versions.toml` has ONE shared catalog entry:\n\n```toml\nrunanywhere = \"0.20.19\"\n...\nrunanywhere-sdk      = { group = \"io.github.sanchitmonga22\", name = \"runanywhere-sdk\",              version.ref = \"runanywhere\" }\nrunanywhere-llamacpp = { group = \"io.github.sanchitmonga22\", name = \"runanywhere-llamacpp\",          version.ref = \"runanywhere\" }\nrunanywhere-onnx     = { group = \"io.github.sanchitmonga22\", name = \"runanywhere-onnx\",               version.ref = \"runanywhere\" }\nrunanywhere-qhexrt   = { group = \"io.github.sanchitmonga22\", name = \"runanywhere-qhexrt-android\",     version.ref = \"runanywhere\" }\n```\n\n**CRITICAL — do not bump this block as one unit.** `runanywhere-qhexrt-android` is\npermanently frozen at whatever version it was last published at before being\nexcluded from the public Maven train; by policy it can never move forward via\npublic Maven again. Verify the actual frozen version FRESH every time (do not\ntrust a remembered number — it doesn't change, but don't skip the check):\n\n```bash\ncurl -s https://repo1.maven.org/maven2/io/github/sanchitmonga22/runanywhere-qhexrt-android/maven-metadata.xml\n```\n\nThe other three artifacts (`runanywhere-sdk`, `-llamacpp`, `-onnx`) CAN and should\nmove to the new release version. This means the single `runanywhere` catalog ref\nmust be **split into two entries** — one at the new version for sdk/llamacpp/onnx,\none pinned to the frozen qhexrt version — not bumped as a block.\n\n**Before trusting a mixed pin works**: verify `RAC_PLUGIN_API_VERSION` (the\nplugin ABI version constant in the C++ core, exposed identically to Kotlin) is\nunchanged between the old public SDK version and the new one. If it changed, mixing\na newer sdk/llamacpp/onnx with the frozen qhexrt version is NOT safe — this is a\nhuman decision (needs either an ABI-compat shim or dropping qhexrt from this build),\nnot something to force through.\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-android\n./gradlew :app:assembleDebug\n./gradlew :app:installDebug        # installs to a connected emulator/device\nadb logcat | grep -i runanywhere   # or a more specific tag if the app uses one\n```\n\nLaunch the app, drive one real `generate()` call against a small downloaded model\n(the app's own model picker/download flow), confirm the resolved SDK version is\nlogged, and review logcat for errors before reporting a PASS.\n\n## 6. Web — `runanywhere-web`\n\n`package.json` has 4 `@runanywhere/*` deps pinned with a caret range:\n\n```json\n\"@runanywhere/proto-ts\":    \"^0.20.19\",\n\"@runanywhere/web\":         \"^0.20.19\",\n\"@runanywhere/web-llamacpp\":\"^0.20.19\",\n\"@runanywhere/web-onnx\":    \"^0.20.19\"\n```\n\nBump all 4 to the new version, then:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-web\nnpm install\nnpm run build\nnpm run dev      # vite --host localhost --port 3000 --strictPort\n```\n\nThe dev/preview server is what supplies COOP/COEP headers required for\n`SharedArrayBuffer` — WASM inference will silently fail (or refuse to init\nthreads) without them, so don't swap in a bare static file server.\n\n`~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks/bindings/web/AGENTS.md`\nhas the authoritative Validation section (its own heading is `## Validation`) —\nfull checklist:\n\n1. Fresh browser context.\n2. Example app served with COOP/COEP headers.\n3. Model download.\n4. Model load.\n5. Real browser inference for the target modality.\n6. Logs/screenshots reviewed.\n\nIts \"Required release gates\" (run before browser validation) from the same repo:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks/bindings/web\nnpm run typecheck && npm run lint && npm run test && npm run build\nnpm run test:browser\nnpm run test:browser:release\ncd example && npm run typecheck && npm run build\n```\n\nStatic gates passing is not the smoke test — you still need an actual browser\nwatching real inference happen against the starter app. Prefer a real browser\nautomation tool (`claude-in-chrome` MCP tools, or Playwright — the maintained\nsuites under `bindings/web/tests/browser/` already use `playwright.config.ts`)\nover curling the served page: WASM inference genuinely needs a browser JS engine.\nLoad the served starter app, download/load a small model, trigger generate, and\nread the actual rendered output text plus the browser console — do not infer\nsuccess from an HTTP 200 on the page load.\n\n## 7. Electron — `runanywhere-electron` (SPECIAL CASE)\n\n**Do not assume a fresh SDK publish is available for this app.** Electron's own\nnpm packages (`@runanywhere/electron`, `-llamacpp`, `-onnx`, `-sherpa`, `-qhexrt`,\n`-neurt`) are on a SEPARATE, manual-publish pipeline — NOT part of `release.yml`\n(grep confirms zero mentions of these package names there). This separation is a\n**confirmed, deliberate** repo-owner decision, not a gap. Check\n`~/.claude/skills/sdk-publish/SKILL.md` §6 before assuming anything moved.\nElectron is **no longer WIP on a branch** — the old \"in-progress on\n`smonga/electron_upgrade`\" note is gone from the root `AGENTS.md`/`CLAUDE.md`\nentirely as of this checkout; `bindings/electron/` is a fully merged SDK on `main`.\nAs of `fix/electron-cicd-staging-url-npu-ane`,\n`.github/workflows/electron-native-package.yml` builds every backend for real and\npackages every tarball — but still does not publish; verify against a live run\nrather than assuming build automation status either way.\n\nTest the starter against whatever version it is CURRENTLY pinned to unless a human\nhas explicitly confirmed a newer Electron publish is ready and intended:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-electron\ncat package.json | grep '@runanywhere/electron'   # confirm current pin before touching it\n```\n\n**`@runanywhere/electron-qhexrt` being public on npm is a CONFIRMED, settled\npolicy decision** (repo owner, 2026-08-20), not a contradiction to flag — it\ncontains real Hexagon NPU binaries (`libQnnHtpV81Skel.so`, `QnnHtp*.dll`,\n`rac_commons.dll`, `runanywhere_qhexrt.dll`) on purpose. The QHexRT-stays-private\npolicy enforced elsewhere in this repo family is specifically about the\nGitHub-Release-side asset set (Android etc.), a different channel. Only re-raise\nthis if you find evidence the policy changed.\n\nTwo layers to actually exercise, do not conflate them:\n\n**(a) Cross-platform Electron JS + macOS native addon** — buildable and testable\nlocally on this Mac right now:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-electron\nnpm install\nnpm run build   # or the app's documented dev/start script — check package.json scripts\nnpm start        # or equivalent — launch the actual Electron app\n```\n\nDrive one real `generate()` call in the running app, watch its console/log output\nfor the resolved SDK version, model download, load, and generation, and read any\nerrors rather than ignoring them.\n\n**(b) Genuine Windows-native behavior** — a macOS build proves NOTHING here\nregardless of how clean the cross-platform code looks; the native addon and any\nWindows-only DLLs must be exercised on real Windows.\n\nThe team keeps a **Windows ARM64 test machine reachable over SSH**. Its hostname,\nuser, key, and VPN/mesh node name are operator-specific and deliberately not\nrecorded in this repo — read them out of the operator's own `~/.ssh/config` (look\nfor the Windows/ARM64 host entry) and use that alias below wherever `$WIN_BOX`\nappears. If no such entry exists, the box is not configured for you: say so and\nask, rather than guessing at a hostname.\n\n```bash\nWIN_BOX=<the ssh alias from ~/.ssh/config>\n```\n\n**Always check reachability first, never just try SSH and retry on timeout.** The\nbox lives on a private mesh VPN and is frequently asleep, so start by asking the\nVPN client whether the node is up (e.g. `tailscale status`, matching on the host\nentry's `HostName`) rather than opening a connection.\n\nIf the node reports `offline, last seen ...`, the box is asleep/powered off.\n**A connection timeout here means \"device asleep,\" not \"config is wrong.\"** Tell\nthe human and ask them to wake it; do not loop retrying SSH. Only once the node\nshows a live/idle state, confirm with:\n\n```bash\nssh -o ConnectTimeout=8 \"$WIN_BOX\" \"whoami\"\n```\n\nThen, with the repo already cloned there:\n\n```bash\nssh \"$WIN_BOX\" \"cd <repo-path-on-that-box>; git pull; npm install; npm run build\"\n# then run whatever launches the Windows Electron build and drive a real generate call\nscp \"$WIN_BOX\":<remote-log-or-artifact-path> ./local-copy   # pull back evidence to inspect\n```\n\nDo not claim Windows-specific validation happened unless you actually obtained a\nshell on that box and ran something real there — a clean macOS build is not\nsubstitute evidence.\n\n**`node_modules/electron` on this box silently tracks whichever arch last ran `npm\ninstall`, not which Node launches the test.** The x64 (llamacpp/onnx/sherpa) and\narm64 (QHexRT) lanes share ONE `runanywhere-electron` checkout/`node_modules` tree.\nPrepending an x64 Node to `PATH` and running `npx playwright test` does NOT make\nElectron itself x64 — `require('electron')` always resolves the single installed\n`node_modules/electron/dist/electron.exe`, and whichever arch that binary actually\nis determines which `prebuilds/win32-<arch>/` directory the running app looks in,\nregardless of `scripts/use-sdk.mjs local`'s staging (which itself is arch-aware\nbased on the *launching* Node, so it can correctly report \"staged win32-x64\" while\nthe process that will actually run is arm64 — a silent mismatch). Verify with:\n`powershell -Command \"$p=[IO.File]::ReadAllBytes('...\\electron.exe'); $peOff=\n[BitConverter]::ToInt32($p,60); [BitConverter]::ToUInt16($p,$peOff+4)\"` — `43620`\n(`0xAA64`) is ARM64, `34404` (`0x8664`) is x64. If it's the wrong arch (e.g. a prior\nsession's `npm install` for the other lane silently overwrote it), a genuine re-test\nof the other lane needs a FULLY SEPARATE checkout/`node_modules` tree installed\nunder the matching-arch Node — don't assume PATH-prepending alone is sufficient, and\ndon't trust `use-sdk.mjs status`'s \"platform: win32-x64\" output as proof the running\nElectron process is actually x64.\n\n**NPU hardware is a scarce, single-owner resource — serialize, never parallelize.**\nThis box has ONE physical Hexagon NPU. `engines/qhexrt/qhexrt_session.cpp`'s\n`session_open()` has a load-bearing comment on this exact failure mode: the HTP\nexposes a small number of protection domains with a per-PD context ceiling, so a\n*second* concurrent model load fails with `RAC_ERROR_BACKEND_INIT_FAILED`\n(\"Backend initialization failed\") — a fast (tens-of-ms), generic-looking failure\nthat is easy to mistake for a real regression. Confirmed once tonight: two agents\neach independently exercising QHexRT on this box at the same time produced exactly\nthis failure. Before concluding a QHexRT test genuinely failed, confirm nothing\nelse (another agent, a leftover Electron process, a stuck previous test run) is\nalso holding an NPU session open on this box — check for lingering `electron.exe`/\n`node.exe` processes from a prior run and kill them if orphaned, and never launch\ntwo NPU-touching verification passes concurrently on this machine.\n\n**\"Backend initialization failed\" has (at least) three distinct root causes —\ndon't stop at PD exhaustion.** Confirmed on 2026-08-17 after PD-exhaustion and\nstaleness were both ruled out (fresh rebuild, source-identical prebuilt, no other\nprocess holding a session): the *actual* bug was two compounding issues, both now\nfixed/documented —\n1. **Real source bug, fixed in PR #733**: `qhexrt_session.cpp`'s `runtime_acquire()`\n   called `qhx_runtime_create(nullptr, nullptr)` on every non-Android platform,\n   trusting the OS default DLL search order to find `QnnHtp.dll`/`QnnSystem.dll`.\n   On Windows that order never includes a dynamically-loaded sibling plugin's own\n   directory (`LOAD_WITH_ALTERED_SEARCH_PATH`, used to load `runanywhere_qhexrt.dll`\n   itself, only widens the search for *that* load call, not later `LoadLibrary`\n   calls made by code already running inside it) — so the QNN DLLs were invisible\n   even sitting right next to the plugin. Fixed by resolving the plugin's own\n   module directory (`GetModuleHandleExA`+`GetModuleFileNameA`) and passing\n   explicit paths, mirroring Android's existing `ADSP_LIBRARY_PATH` resolution in\n   the same file.\n2. **Operational staleness, NOT a script bug**: even after (1), the QNN runtime\n   DLLs actually staged into `@runanywhere/electron-qhexrt`'s\n   `prebuilds/win32-arm64` (`node_modules/.../electron-qhexrt/prebuilds/win32-arm64/`)\n   were from an old QAIRT build (~2.45.x vintage, Dec 2025 files) while the box's\n   installed QAIRT was 2.48.0.260626 — a real ABI-relevant version mismatch.\n   `bindings/electron/scripts/bundle-native.ts`'s `stageQnnRuntime()` just mirrors\n   whatever `RA_QNN_RUNTIME_DIR` points at with **no version check** — it's\n   correct-as-written; someone had pointed it at a stale neurun-side\n   `qairt-runtime-248`-named folder that was never refreshed. Fix: rebuild that\n   flat dir from the CURRENT QAIRT SDK's `lib/aarch64-windows-msvc/` (host DLLs:\n   `QnnHtp.dll`, `QnnHtpPrepare.dll`, `QnnHtpV81Stub.dll`,\n   `QnnHtpV81CalculatorStub.dll`, `QnnSystem.dll`) + `lib/hexagon-v81/unsigned/`\n   (DSP side: `libQnnHtpV81Skel.so`, `libqnnhtpv81.cat`), point\n   `RA_QNN_RUNTIME_DIR` at it, and re-run\n   `npm run bundle:native -- --package=qhexrt` from `bindings/electron/`.\n   **The already-published `@runanywhere/electron-qhexrt` npm package (as of\n   0.20.24) almost certainly ships the same stale DLLs** — this needs a\n   human decision (republish/patch) before claiming the published package works\n   on Windows NPU, separate from the source fix in PR #733.\n3. **PD exhaustion** (the pre-existing entry above) remains real and still applies\n   when two sessions genuinely contend for the one physical NPU — but it is not\n   the only explanation, and the log line to actually check first is the native\n   one: subscribe via `window.runanywhere.logging.records()` (after\n   `logging.setDebugMode(true)`) or add a temporary main-process log capture —\n   `app.process().stdout`/`stderr` do NOT carry it, because the addon runs in a\n   separate `utilityProcess.fork()` host, not the Electron main process, and\n   `RAC_LOG` is not an env var the logger reads (it does nothing). The generic\n   `[QHexRT] qhx_runtime_create failed (QNN libs unavailable?)` vs.\n   `qhx_model_load failed: ... N NPU model(s) already resident` log text is what\n   actually distinguishes cause (1)/(2) from cause (3) — always get that line\n   before diagnosing further.\n\n**Workflow gotcha:** if you orchestrate this via the Workflow tool, its agent()\ncalls do NOT reliably block-wait on a long-running remote build the way a direct\npoll loop does — observed once tonight where a workflow's own agents spawned an\ninternal Monitor/poller for the build log and then returned immediately,\nreporting \"still waiting,\" which let the whole workflow finish with nothing\nactually verified. For \"wait N tens-of-minutes for a real native build to\nfinish,\" use the top-level Monitor tool yourself (a shell poll loop over SSH\nchecking for a terminal BUILD_SUCCEEDED/BUILD_FAILED marker line) rather than\ndelegating the wait itself into a workflow agent's own judgment.\n\n### The Windows box's actual layout, and why it's more than a cross-platform check\n\n**This is a Hexagon-NPU-equipped Windows machine** (Snapdragon-class ARM Windows\ndevice, Copilot+ PC), not just a generic x86 Windows box. That makes it the one\nplace a QHexRT/Hexagon backend build+test can produce real device-only truth for\nWindows — matching the parent workspace's own \"x86 is a simulator, device-only\ntruth\" rule (see the top-level `Qualcomm/CLAUDE.md` and `QHexRT/CLAUDE.md`). Don't\ntreat this box as merely a Windows Electron smoke-test target; it's the only real\nHexagon NPU test rig reachable for this platform.\n\nPer the user (2026-08-17), under `~/Downloads/RunAnywhere/` on that box there are\nthree sibling repo checkouts at the same level — **verify each on connect, don't\nassume paths without checking, since this is relayed secondhand and may have\ndrifted**:\n- `runanywhere-sdks` — this monorepo, presumably already cloned; `git fetch && git\n  log -1 origin/main` to see how far behind it is before doing anything.\n- `runanywhere-electron` — the Electron starter app (same repo as\n  `RunanywhereAI/runanywhere-electron` elsewhere in this skill).\n- a third repo referred to verbally as \"the neuron/new-run inference engine for the\n  Hexagon NPU\" — almost certainly **`neurun`**, the QHexRT/NeuRT split-out repo\n  (see memory: \"neurun component split merged — QHexRT/ + NeuRT/ + shared/, root is\n  a closed 20-entry set enforced by a test\"). Confirm the actual directory name and\n  remote (`git remote get-url origin`) on connect rather than guessing from the\n  transcription.\n\n**Full task on this box (per explicit user instruction, not just a smoke test):**\n1. `git pull` (or fetch+fast-forward) all three repos to latest `main`.\n2. Rebuild the QHexRT/Hexagon NPU backend from `neurun` against this device's real\n   Hexagon NPU — this is the one environment where that build+run is meaningful\n   proof, not a simulator claim.\n3. Bump Electron's native dependencies to their latest versions — llama.cpp,\n   sherpa-onnx, ONNX Runtime, and whatever else `bindings/electron` pins — across\n   the repos involved. Treat this as a real dependency-bump task (check for\n   breaking API changes, don't just edit a version string and assume it builds).\n4. Update the model catalog for **all 5 starter apps across all 4 consumer repos**\n   (iOS, Android, Web, Electron — plus whatever the \"5th app\" split means for\n   macOS/Swift, see the rest of this skill) so the catalog is consistent everywhere,\n   not just on this Windows box.\n5. Run a real end-to-end inference pass on this box specifically (not just build\n   success) — both the general Electron/llama.cpp path AND the Hexagon NPU path via\n   `neurun`/QHexRT.\n6. Open PRs for whatever changed, in each affected repo. Do not merge them —\n   the user explicitly said they'll review.\n\nThis is real engineering work (native dependency bumps + an NPU backend rebuild),\nnot a mechanical version-string edit — expect it to take real time and to\ngenuinely need `QHexRT/CLAUDE.md` and `neurun`'s own docs read in full before\ntouching that backend, per the parent workspace's own instructions.\n\n## 8. Reporting\n\nFor each of the 5 apps (iOS, macOS/Swift, Android, Web, Electron), state exactly\none of these two outcomes — never blur them:\n\n- **FULLY VERIFIED** — with concrete evidence: the version string actually\n  resolved/logged, the model used, and the actual output produced (quote it).\n- **BLOCKED** — with the exact concrete reason, e.g.: the Windows ARM64 box's\n  mesh-VPN node is offline; Android emulator unavailable in this environment; App Store /\n  notarization credential missing; `runanywhere-swift` not yet tagged at target\n  version so `swift package update` can't resolve; electron-qhexrt policy conflict\n  needs a human call before proceeding further.\n\nA clearly reported partial/blocked result is far more valuable than an optimistic\nsummary that overstates what was actually observed. If you hit the known Electron\nqhexrt npm-publish policy conflict (§7), call it out in the report even if it\nwasn't the thing blocking your specific test — it's a standing issue every release\nnight should re-surface until a human resolves it.\n\n## Credentials referenced but never printed by this skill\n\nThese exist and are checked for presence only — never print their values:\n\n- Maven Central: `mavenCentral.username` / `mavenCentral.password`,\n  `signing.gnupg.keyName` / `signing.gnupg.passphrase` in `~/.gradle/gradle.properties`.\n- GPG signing key `CC377A9928C7BB18` — check with\n  `gpg --list-secret-keys --keyid-format LONG`.\n- macOS notarization profile `runanywhere-notary` — check with\n  `xcrun notarytool history --keychain-profile runanywhere-notary`; codesign\n  identity `Developer ID Application: RunAnywhere, Inc` — check with\n  `security find-identity -v -p codesigning`.\n- npm — `npm whoami` should already show a logged-in user.\n\nNone of the app-level smoke tests in this skill require touching these directly;\nthey matter only if a test path requires a fresh distro-repo publish (e.g.\n`runanywhere-swift` not yet tagged) — that's a separate concern of the\n`sdk-publish` skill, not this one.\n","frontmatter":{"name":"sdk-test-starters","description":"Run a REAL end-to-end inference smoke test (model download + load + generate, with visible output) on every RunAnywhere starter app — iOS, macOS/Swift, Android, Web, Electron — after a runanywhere-sdks release. Build success alone is never sufficient evidence."},"isInternal":false,"tokens":7370,"sizeBytes":28933},{"name":"SKILL.md","path":".agents/skills/telemetry-e2e/SKILL.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/.agents/skills/telemetry-e2e/SKILL.md","title":"telemetry-e2e","category":"anthropic-skill","format":"markdown","content":"---\nname: telemetry-e2e\ndescription: Prove that what an SDK binding EMITS on the telemetry wire is correct and that it actually arrives — vocabulary compliance, presence/absence semantics, device-id stability, and a received request body / stored row as evidence. Use when touching telemetry_json.cpp, telemetry_manager.cpp, rac_telemetry_vocabulary.h, any binding's platform-adapter telemetry bridge, or when asked to verify a telemetry field isn't silently dropping.\n---\n\n# Telemetry Wire Correctness (SDK side)\n\nThis is the SDK-side counterpart to the backend's `telemetry-e2e` skill (private\n`Runanywhere-monorepo` repo — proves event-to-stored-row on the server side). This one\nproves the other half: that commons *serializes* a call correctly and that the bytes\n*leaving* a binding are the bytes a real backend accepts. It does not cover per-starter-app\ninference smoke tests — see `sdk-test-starters` for that.\n\n## 0. The evidence bar\n\nCompiling is not evidence. A green `telemetry_extraction_tests` run is not evidence that a\nreal binding's real call site reaches the wire correctly — it only proves the *serializer*\nhandles a hand-built payload correctly. In order of increasing strength:\n\n1. **Unit test green** (`ctest -R telemetry_extraction_tests`) — proves the JSON builder's\n   presence/vocabulary rules are still correct in isolation. Necessary, not sufficient.\n2. **Received request body** — capture the actual JSON `rcli` (or a real binding) put on\n   the wire and read it: right modality endpoint, right keys, no vocabulary values that\n   got through that shouldn't have, measured zeros kept as `0` not `null`.\n3. **Stored row matches, field-for-field** — the backend's own ingest response\n   (`events_received`/`events_stored`/`events_skipped`) or a direct DB read shows the row\n   landed with the values you actually sent, not just a 2xx. This is the only tier that\n   also proves the vocabulary/schema on both sides still agree.\n\nA PR description that says \"sent telemetry, no errors\" without quoting a request body or a\nstored-row count is not evidence of anything except that a socket didn't error out.\n\n## 1. The emit path\n\nEvery binding funnels through the same two C ABI entry points in\n`core/src/infrastructure/telemetry/telemetry_manager.cpp`:\n\n- `rac_telemetry_manager_track` — flat-struct payload (`rac_telemetry_payload_t`), used by\n  `rcli`'s `telemetry emit`/`telemetry blast` and by hand-built events.\n- `rac_telemetry_manager_track_proto` — the real path every language binding actually uses:\n  a binding builds a protobuf `SDKEvent` (`sdk_events.pb.h`) at the real call site (e.g. an\n  LLM `generate()` completing), and `proto_event_type_string()` + the per-domain extraction\n  blocks in `telemetry_manager.cpp` turn it into the same flat payload. `framework_proto_to_string()`\n  / `framework_to_string()` / `clean_framework()` do the backend-name normalization here —\n  this is where a raw enum like `INFERENCE_FRAMEWORK_LLAMA_CPP` becomes the wire string `\"llamacpp\"`.\n- `stamp_live_device_state()` (same file) then overwrites the SDK-origin/device-state fields\n  (`sdk_binding`, `platform`, `battery_state`, memory, core count) from manager-owned state,\n  not from whatever the caller passed — a binding cannot inject a platform or binding value.\n- The payload is handed to `rac_telemetry_manager_payload_to_json()` in\n  `core/src/infrastructure/telemetry/telemetry_json.cpp`, which does field-by-field\n  serialization (see §2/§3 for the two contracts enforced there).\n- `send_batch_json()` batches by modality and POSTs to `/api/v2/sdk/telemetry/{modality}`\n  through whichever `rac_http_transport_ops_t` the binding registered (URLSession/OkHttp/\n  `emscripten_fetch`/curl for `rcli` — see the root `AGENTS.md`'s \"HTTP is platform-provided\"\n  rule). There is no `libcurl` fallback baked into commons itself.\n\nIf a field is going missing on the wire, the fix is almost always in\n`telemetry_json.cpp` (a field never wired into the serializer) or in the call site's proto\npopulation (a field never set on the `SDKEvent`) — not in a binding's bridge code, which\njust forwards bytes.\n\n## 2. The closed vocabularies — and the silent-drop rule\n\n`core/include/rac/infrastructure/telemetry/rac_telemetry_vocabulary.h` is a **generated\nfile** (see §5) holding six closed tables, each with a `RAC_TELEMETRY_<NAME>_COUNT` and a\n`RAC_TELEMETRY_<NAME>_VALUES[]` array:\n\n| Table | Count | Wire field | Values |\n|---|---|---|---|\n| `RAC_TELEMETRY_EVENT_TYPE` | 106 | `event_type` | namespaced strings, e.g. `llm.generation.completed`, `auth.succeeded`, `stt.transcription.completed` |\n| `RAC_TELEMETRY_BACKEND` | 22 | `backend` / `framework` | `onnx, sherpa, llamacpp, foundation_models, system_tts, fluid_audio, coreml, whisperkit_coreml, mlx, qhexrt, neurt, tflite, executorch, mediapipe, mlc, pico_llm, piper_tts, swift_transformers, cloud, builtin, none, unknown` |\n| `RAC_TELEMETRY_MODEL_SOURCE` | 3 | `model_source` | `catalog, builtin, user` |\n| `RAC_TELEMETRY_PLATFORM` | 9 | `platform` | `ios, android, macos, windows, linux, web, tvos, watchos, visionos` |\n| `RAC_TELEMETRY_SDK_BINDING` | 9 | `sdk_binding` | `swift, kotlin, flutter, react-native, web, electron, python, cli, cpp` |\n| `RAC_TELEMETRY_BATTERY_STATE` | 4 | `battery_state` | `charging, full, unplugged, unknown` |\n\n**The rule, verbatim from `add_vocabulary_string()` in `telemetry_json.cpp`:** if a value is\nnon-null but not present in its table, the field is dropped to `null` and the *rest of the\nevent still ships*. This is by design (a bad `backend` string must not cost the whole\nobservation) but it means:\n\n- The backend never sees the bad value, never 422s, never quarantines anything — there is\n  no error path anywhere for this. A field just isn't there.\n- The only client-side signal is a `RAC_LOG_WARNING(\"Telemetry\", \"Dropping %s: '%s' is not\n  in the published vocabulary\", ...)` log line — easy to miss unless you're specifically\n  watching debug logs.\n- `platform`, `sdk_binding`, and `battery_state` are stamped by the manager itself (§1), not\n  supplied by a binding's call site, so in practice only `backend`/`framework` (from the\n  engine/model layer) and `event_type` (only reachable via the proto path, checked at CI\n  time — see §5) are realistic places a new value can silently disappear.\n- `test_telemetry_extraction.cpp`'s vocabulary block (`\"vocab-1\"`/`\"vocab-2\"` cases) is the\n  concrete regression guard: it sends `framework = \"LlamaCpp\"` (a real spelling that used to\n  reach production) and asserts the string never appears on the wire while `model_id` still\n  does. If you touch `add_vocabulary_string()` or add a new vocabulary-gated field, extend\n  that test rather than trusting a manual check.\n\nHistorical motivation (from the header's own comment): before this table existed, stored\nproduction rows had four spellings of `llama.cpp`, three of ONNX, and binding names like\n`\"react-native\"` recorded as a `platform` — this file exists specifically to make that class\nof drift fail in this repo instead of being discovered in a database nobody was watching.\n\n## 3. Presence vs. absence: the \"measured zero\" contract\n\nBefore the fix documented in `telemetry_json.cpp`'s header, extractors read proto3 scalar\nfields directly — which report `0` for a field that was never set — and the old serializer\n(`add_int`/`add_double`) *skipped* zero values to keep payloads small. Result: a genuinely\nmeasured `output_tokens = 0` (an empty completion) and an unmeasured `tokens_per_second`\nboth arrived on the wire as absent, indistinguishable from each other.\n\nThe fix is `has_x`-gated serialization. Two helper families in `telemetry_json.cpp`:\n\n- `add_int_or_null(key, value, is_valid)` / `add_double_or_null(key, value, is_valid)` —\n  emit the real value (including `0`) when `is_valid` is true, else the JSON literal `null`.\n  Non-finite doubles (`NaN`/`Inf`) always degrade to `null` regardless of `is_valid`, because\n  JSON has no such literal and one bad value must not corrupt the whole batch.\n- The `is_valid` argument is a real `has_x`/sentinel check per field, not a blanket flag —\n  e.g. `payload->has_processing_time_ms != RAC_FALSE`, `payload->total_memory > 0`,\n  `payload->battery_level >= 0`.\n\n**What to check to confirm this stayed fixed** (this is exactly what\n`test_telemetry_extraction.cpp`'s presence block does — see §4): send an LLM completion\nevent with `output_tokens = 0` and `input_tokens = 0` explicitly set, and\n`tokens_per_second`/`context_length` left unset. Assert the wire body contains\n`\"output_tokens\":0` and `\"input_tokens\":0` as literal numbers, and `\"tokens_per_second\":null`\n/ `\"context_length\":null` — not all four as `null`, and not all four as `0`. If a future\nrefactor reverts to plain `add_int`/`add_double` for any of these fields, that regression\ntest is the thing that catches it; grep `telemetry_json.cpp` for `add_int(` / `add_double(`\n(the non-`_or_null`/non-`_always` forms) if you suspect a field regressed — those two still\nsilently skip zero by design and must never be used for a field where `0` is meaningful.\n\n## 4. Build, test, and what each telemetry test asserts (macOS)\n\n```bash\ncmake --preset macos-debug\ncmake --build build/macos-debug\nctest --preset macos-debug --output-on-failure -R 'telemetry|device_identity'\n```\n\nOr the non-preset form (`core/AGENTS.md`'s documented path):\n\n```bash\ncmake -B build -DRAC_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Debug\ncmake --build build\nctest --test-dir build --output-on-failure\n```\n\n`telemetry_extraction_tests` and `device_identity_tests` are registered in\n`core/tests/CMakeLists.txt`; `rcli_telemetry_live_tests` is registered separately in\n`rcli/tests/CMakeLists.txt`. All three are hand-rolled assertion runners — `CHECK()` in\nsome files, `ASSERT_EQ`/`ASSERT_TRUE` (from `core/tests/test_common.h`) in others — never\nGoogleTest:\n\n| ctest name | Binary | What it asserts |\n|---|---|---|\n| `telemetry_extraction_tests` | `test_telemetry_extraction` | Full proto→JSON pipeline per modality (LLM token counts/`tokens_per_second`, STT NaN-confidence never leaks as the string `\"nan\"`, TTS `characters_per_second`, embeddings `total_tokens`/`batch_size`/`embedding_dimension`, LoRA failure-path fields, RAG retrieval counts, VLM image/timing fields, live device-state stamping); the presence/measured-zero contract (§3); the closed-vocabulary drop rule (§2); and an HTTP-retry backoff (a failed batch is re-sent only after >5s, never before). Requires `RAC_HAVE_PROTOBUF` — the test binary itself no-ops with exit 0 if commons wasn't built with protobuf support. |\n| `device_identity_tests` | `test_device_identity` | Every branch of `rac_device_get_or_create_persistent_id()`'s resolution chain (§6) against a mocked platform adapter — cache hit, vendor-ID fallback, fresh-UUID generation, and that a `secure_set` failure doesn't silently drop the ID. |\n| `rcli_telemetry_live_tests` | `test_rcli_telemetry_live` | **Opt-in, always registered but a no-op by default.** Runs only with `--live` *and* `RUNANYWHERE_BASE_URL`/`RUNANYWHERE_API_KEY` set; otherwise prints \"skip\" and exits 0 — a green ctest run for this target proves nothing by itself. When live, it runs `rcli::bootstrap()` (real API-key → device-register → JWT handshake), sends one real event per modality (LLM, embeddings, RAG, VLM, LoRA-failure) through the real HTTP transport, and fails on any non-2xx response — this is the test that would catch a strict-schema `422 extra_forbidden` from field drift against the real V2 endpoints. |\n\nRun it live (§7 has the full local-backend flow):\n\n```bash\nRUNANYWHERE_BASE_URL=https://<backend-origin> RUNANYWHERE_API_KEY=<key> \\\n  ./build/macos-debug/rcli/tests/test_rcli_telemetry_live --live\n```\n\n`test_platform_bridge` (same `CMakeLists.txt`, adjacent target) is the wider companion\ncheck — \"every value a binding supplies must reach the wire\" — covering the platform half\nof the same two flat C structs (`rac_client_info_t`/`rac_device_registration_info_t`) for\nevery SDK at once; run it alongside if you're touching client-info fields rather than\ntelemetry event fields specifically.\n\n## 5. Regenerating the vocabulary header, and the drift gate\n\n`rac_telemetry_vocabulary.h` (§2) is generated from `idl/http/sdk-openapi.json` — the\ndevice-facing OpenAPI subset copied in from the private backend repo. **How that file gets\nupdated on the backend side, and how it crosses into this repo, is the monorepo's\n`openapi-contract` skill's job — read that by name in the other repo rather than\nre-deriving the steps here.** From inside this repo, once `idl/http/sdk-openapi.json` is\ncurrent:\n\n```bash\npython3 idl/codegen/generate_telemetry_vocabulary.py          # regenerate the header\npython3 idl/codegen/generate_telemetry_vocabulary.py --check  # CI drift check; exit 1 if stale\n```\n\n`.github/workflows/telemetry-vocabulary-drift.yml` runs three checks on every push to\n`main`/`development` and on any PR touching `idl/http/**`, the codegen script, or the\ntelemetry source/header dirs:\n\n1. **Generated header is current** — the `--check` command above.\n2. **Every event type the core can emit is in the vocabulary** — a Python check that parses\n   `proto_event_type_string()`'s body out of `telemetry_manager.cpp` via regex, unions in\n   the two prefix-expansion families (`stt.model.*`/`tts.voice.*`/`llm.model.*` and\n   `llm*`/`vlm*` generation suffixes), and diffs against `TelemetryEventType`'s enum in\n   `idl/http/sdk-openapi.json`. Anything emittable but unpublished fails the job.\n3. **Every framework string the core can emit is in the vocabulary** — same technique\n   against `framework_proto_to_string()` and `framework_to_string()` vs. `TelemetryBackend`.\n\n**A documented drift you will hit if you trust the wrong file**: `idl/http/README.md`'s own\n\"What it pins\" table names the schema for the `framework`/`backend` field as\n`TelemetryFramework` — that's stale. The actual generator\n(`idl/codegen/generate_telemetry_vocabulary.py`'s `VOCABULARIES` dict) and the header it\nproduces both say `TelemetryBackend`. Trust the generator and the generated header's own\n`// Published as X` comments over that README table.\n\n## 6. Per-binding device identity: where it's created, where it's persisted\n\nThe resolution chain itself is centralized in commons —\n`core/src/infrastructure/device/device_identity.cpp`\n(`rac_device_get_or_create_persistent_id`, tested by `device_identity_tests`, §4) — and is\nthe same fixed order for every binding:\n\n1. `adapter.secure_get(\"com.runanywhere.sdk.device.uuid\")` — if found and non-empty, use it,\n   full stop.\n2. `adapter.get_vendor_id` (if the platform adapter provides one) — use it, then persist it\n   via `secure_set` so step 1 hits next boot.\n3. Generate a fresh RFC-4122 v4 UUID, persist it via `secure_set`.\n\nWhat differs per binding is **only** the `secure_get`/`secure_set` backing store — an\nunstable backing store (one that doesn't survive a real app restart, or that resolves to a\ndifferent physical location than the read path expects) is exactly the bug class that\ninflates device counts, because step 3 fires again on every \"cache miss\":\n\n| Binding | Backing store | Where |\n|---|---|---|\n| Swift (iOS/macOS) | Keychain, service `com.runanywhere.sdk` | `bindings/swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+PlatformAdapter.swift` (`platformSecureGetCallback`/`platformSecureSetCallback`) |\n| Kotlin (Android) | Android Keystore AES-256-GCM key, ciphertext in a no-backup app file (not `SharedPreferences`) | `bindings/kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/security/AndroidKeychainManager.kt` (`NoBackupCiphertextStore`), wired through `CppBridgePlatformAdapter.PlatformSecureStorage` |\n| Flutter | Native Keychain/Keystore helper via FFI symbols `ra_flutter_secure_storage_{store,retrieve,delete}` | `bindings/flutter/packages/runanywhere/lib/native/dart_bridge_secure_storage.dart`; the device-UUID key constant is duplicated (deliberately, per its own comment) as `_keyDeviceUUID = 'com.runanywhere.sdk.device.uuid'` in `dart_bridge_device.dart` |\n| React Native (Android) | `SecureStorageManager` (native Android side) via `PlatformAdapterBridge.kt`'s `secureGet`/`secureSet` | `bindings/react-native/packages/core/android/src/main/java/com/margelo/nitro/runanywhere/PlatformAdapterBridge.kt` |\n| Web | `localStorage`, key `rac_sdk_plaintext_com.runanywhere.sdk.device.uuid` — **plaintext**, no OS-level encryption equivalent exists in a browser; restricted by convention to non-sensitive IDs | `bindings/web/packages/core/src/runtime/PlatformAdapter.ts` (`registerSecureGet`/`registerSecureSet`); also has a separate `stableVendorId()` fallback under key `rac_sdk_plaintext_vendor_id` |\n| Electron (macOS/Linux) | Flat file under a secure directory, path-traversal-guarded (`fs::path(key)` must not be absolute or contain `..`) | `bindings/electron/native/posix_platform_adapter.cpp` (`posix_secure_get`/`posix_secure_set`) |\n| Electron (Windows) | Windows DPAPI (`CryptProtectData`, current-user scope, no UI) | `bindings/electron/native/win32_platform_adapter.cpp` (`win_secure_get`/`win_secure_set`) |\n\nThe key string itself — `com.runanywhere.sdk.device.uuid` — is intentionally the same\nliteral across Swift, Flutter, and RN (each source comments cross-reference the other two by\nname), so grepping for it is a fast way to confirm a new binding is reading/writing the\n*same* key it thinks it is, not a typo'd sibling.\n\n**How to verify stability across a real restart** (not just \"the code looks right\"):\n\n1. Run the binding once, capture the emitted `device_id` from a real request body (§7) or\n   from `POST /api/v1/devices/register`'s response.\n2. Fully terminate the process (not just background it — on mobile this matters, a\n   backgrounded app can keep its in-memory device-id cache alive without ever touching\n   the read path again).\n3. Relaunch and repeat step 1.\n4. The `device_id` must be byte-identical. If it changed, the break is almost always one of:\n   the backing store not surviving process death (e.g. an in-memory-only stub used during\n   development), a key-name mismatch between the write and read paths, or (Android\n   specifically) `Context.deleteSharedPreferences` being invoked on the wrong file — see\n   `AndroidKeychainManager.kt`'s constructor, which deletes a hardcoded *legacy* prefs name\n   on every construction; if that name were ever widened to match the real store, every\n   Android launch would look like a fresh device.\n\nTwo-in-one launches on the *same physical device* (e.g. two emulator instances, or a\nsame-device reinstall vs. a real fresh install) are a different, real-world failure mode\nfrom a code bug — don't conflate an intentional reinstall's fresh ID with instability.\n\n## 7. Point a local build at a local backend and prove an event landed\n\n`rcli` is the fastest way to exercise the full wire path without a mobile/browser\ntoolchain — it links `rac_commons` directly and speaks real HTTP via curl.\n\n```bash\ncmake --preset rcli-macos-release   # or rcli-linux-release on Linux\ncmake --build build/rcli-macos-release --target rcli -j \"$(sysctl -n hw.logicalcpu)\"\n```\n\n**Keyless (development environment) — no API key, works against any backend that allows\nanonymous V2 telemetry:**\n\n```bash\n./build/rcli-macos-release/rcli/rcli --environment development \\\n  --base-url http://127.0.0.1:8000 \\\n  telemetry blast --processing-ms 42.5 --session-id my-check \\\n  --input-tokens 128 --output-tokens 256\n```\n\nThis is exactly the shape `scripts/ci/oss_keyless_telemetry_blast.sh` runs in CI against the\npublic staging backend — `--environment development --base-url <origin> telemetry blast\n...` with no `--api-key`. `telemetry blast` sends one representative event per modality (all\n12) and prints a `MODALITY | RESULT | STATUS | RECEIVED | STORED | SKIPPED` table parsed\ndirectly from the backend's own batch response — **the `STORED` column is authoritative\nserver-side confirmation**, not just an HTTP 2xx. `run_telemetry_blast()`'s own exit-code\nlogic (`rcli/src/commands/cmd_telemetry.cpp`) does fold `stored >= count` into its overall\npass/fail, so a genuine storage shortfall on any modality does make the process exit\nnon-zero, not just a transport error — but the exit code is one bit for all 12 modalities\ncombined, so a non-zero exit still requires reading the table to know *which* modality\nfailed and *why* (a `network error` status is a transport problem; an `HTTP 200` row with\n`STORED` under `RECEIVED` is a row-level rejection, e.g. a vocabulary value the backend\ndidn't recognize — a real bug to chase, not the same failure class at all).\n\n**Authenticated (production shape) — for exercising the login handshake too:**\n\n```bash\n./build/rcli-macos-release/rcli/rcli --environment production \\\n  --base-url https://<backend-origin> --api-key <key> \\\n  auth login\n./build/rcli-macos-release/rcli/rcli --environment production \\\n  --base-url https://<backend-origin> --api-key <key> \\\n  telemetry emit --modality llm --count 3 --input-tokens 128 --output-tokens 256\n```\n\n`auth login` only has a path in `production` — `development`'s keyless mode has no login\nstep by design (see `rcli/src/commands/cmd_auth.cpp`'s own subcommand help text).\n\n**To inspect the actual request body**, not just the summary table, add `--json` for\nmachine-readable CLI output and `-v` for debug logging (the debug log includes the\nserialized JSON per batch before it goes on the wire) — or point `--base-url` at a throwaway\nlocal capture endpoint (any script that logs the raw POST body) before pointing it at a real\nbackend, to sanity-check field shape offline first.\n\n**If you have the private backend repo checked out locally too**, running it yourself\n(`cd backend && uv run uvicorn main:app --reload`, per that repo's own README) and pointing\n`--base-url` at `http://127.0.0.1:8000` gets you the strongest possible evidence: query the\nSQLite/Postgres row directly after a blast and diff it field-for-field against what you sent\n— the monorepo's own `telemetry-e2e` skill (its integration suite, hermetic pytest fixtures,\nand `test_telemetry_conformance.py`'s vocabulary/presence/identity assertions against stored\nrows) is the authoritative reference for that side; don't re-implement its DB assertions\nhere, just point at it.\n\n## Evidence bar, restated\n\n- Compiling `rcli` or a binding: **not evidence**.\n- `ctest -R telemetry_extraction_tests` green: **not evidence of a real call site** — only\n  of the serializer in isolation.\n- `rcli telemetry blast` exiting 0: real signal (its exit code does fold in the backend's\n  `stored >= count` check, §7) but still worth pairing with the per-modality\n  `STORED`/`SKIPPED` table in your report — the exit code can't tell you *which* modality\n  or *why* a shortfall happened.\n- A captured request body showing the right endpoint, the right keys, correct\n  presence/absence per §3, and no off-vocabulary values per §2 — **evidence of correct\n  emission**.\n- That request body's values matching a stored row (via the backend's own\n  `events_stored` count at minimum, a direct DB read at best) — **evidence of correct\n  end-to-end delivery**, the only tier worth reporting as \"telemetry works.\"\n","frontmatter":{"name":"telemetry-e2e","description":"Prove that what an SDK binding EMITS on the telemetry wire is correct and that it actually arrives — vocabulary compliance, presence/absence semantics, device-id stability, and a received request body / stored row as evidence. Use when touching telemetry_json.cpp, telemetry_manager.cpp, rac_telemetry_vocabulary.h, any binding's platform-adapter telemetry bridge, or when asked to verify a telemetry field isn't silently dropping."},"isInternal":false,"tokens":5937,"sizeBytes":23363},{"name":"SKILL.md","path":".claude/skills/engine-plugin-authoring/SKILL.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/.claude/skills/engine-plugin-authoring/SKILL.md","title":"engine-plugin-authoring","category":"anthropic-skill","format":"markdown","content":"---\nname: engine-plugin-authoring\ndescription: Step-by-step recipe for adding a new engine plugin under runanywhere-sdks/engines/, or adding a modality (e.g. TTS) to an existing engine (llamacpp, sherpa, onnx, cloud, mlx, neurt, qhexrt). Use when the task is \"add engine X\", \"wire up backend Y\", or \"make engine Z also serve <modality>\".\n---\n\n# Engine plugin authoring (`engines/`)\n\nTwo related runbooks: adding a brand-new engine plugin, and adding a modality to one\nthat already exists. Read `engines/AGENTS.md` first for the concepts these steps\nassume (op-table vtable, manifest, the valid-engine checklist, the 4-file skeleton,\nthe engine↔runtime rule). This skill is the \"how\", that file is the \"what/why\".\n\n**Invariant that applies to both procedures:** never invent a new modality just to\nship an engine — fill an existing `rac_engine_vtable_t` slot. Adding a brand-new\n*primitive* (a new `RAC_PRIMITIVE_*` + vtable slot) is a commons ABI change, covered\nby `core/AGENTS.md` → \"Adding a new capability interface\", not an engine change.\n\n---\n\n## Procedure A — add a new engine\n\nMirror **sherpa** (multi-modality, standalone `.so`) or **cloud** (single modality,\nno runtime) as your template; both are the cleanest references for the 4-file\nskeleton described in `engines/AGENTS.md`.\n\n1. **Create `engines/<name>/`** with the 4-file skeleton: `rac_plugin_entry_<name>.cpp`,\n   `rac_backend_<name>_register.cpp` (skip it if there's no extra bring-up, like\n   `neurt`), `rac_static_register_<name>.cpp`, plus your impl files.\n2. **Write the manifest** in `rac_plugin_entry_<name>.cpp`:\n   - `name` = snake_case identity (the library/framework you wrap, or your own\n     codebase unit — NEVER a modality name; see the naming rule in\n     `engines/AGENTS.md`).\n   - `primitives[]` = what you serve; fill the matching vtable slots; leave every\n     other slot explicit NULL (the all-NULL tripwire depends on this).\n   - `runtimes[]` — only devices your compute actually depends on (NULL for\n     cloud/HTTP engines) — see THE RULE in `engines/AGENTS.md`.\n   - `formats[]` = the `RAC_MODEL_FORMAT_ID_*` values you accept (NULL if there's\n     no local model file).\n   - `priority`, `availability` (PUBLIC/PRIVATE), `package_owner`/`package_name`.\n3. **Implement the op-table(s)** honoring the MODEL LIFECYCLE:\n   `create → initialize → use → cleanup → destroy` (see `engines/AGENTS.md` for the\n   exact signatures). Use `RAC_DEFINE_CREATE_ADAPTER(primitive, name)`\n   (`core/include/rac/plugin/rac_plugin_entry.h`) when `create` is a plain forward\n   onto `rac_<primitive>_<name>_create` — sherpa uses it for STT/TTS/VAD. Engines\n   with richer create flows (llamacpp, onnx, neurt, qhexrt) hand-write it.\n4. **`capability_check`** when the engine is platform- or binary-gated: use the\n   shared 3-way helper `rac_engine_unavailable_capability(platform_supported,\n   backend_present)` from `engines/common/rac_engine_unavailable.h`. When the\n   backend can be entirely absent at build time (a private prebuilt archive, a\n   sibling private repo), emit the stub with `RAC_ENGINE_UNAVAILABLE_PLUGIN(name,\n   display, cap_fn)` from the same header — see qhexrt and neurt for the two-mode\n   (ROUTABLE vs STUB) pattern this produces.\n5. **Registration carriers:** `RAC_PLUGIN_ENTRY_DEF(<name>)` returning the vtable; a\n   `rac_backend_<name>_register()`/`_unregister()` pair if you have extra bring-up\n   (called directly by SDK bridges on dynamic-link hosts); a\n   `rac_static_register_<name>.cpp` shim using `RAC_STATIC_REGISTER_BACKEND(<name>)`\n   (routes through the register fn) or `RAC_STATIC_PLUGIN_REGISTER(<name>)` (calls\n   the entry directly) for iOS/WASM static hosts.\n6. **CMake:** call `rac_add_engine_plugin(<name> SOURCES … LINK_LIBRARIES …\n   AVAILABILITY … PACKAGE_OWNER … PACKAGE_NAME …)` (`cmake/plugins.cmake`) from\n   `engines/<name>/CMakeLists.txt`, fronted by your own\n   `option(RAC_BACKEND_<NAME> \"…\" <default>)` + `if(NOT RAC_BACKEND_<NAME>)\n   return() endif()` self-gate (each engine owns its own option — `engines/CMakeLists.txt`\n   just unconditionally `add_subdirectory()`s and lets the child decide). Add\n   `add_subdirectory(<name>)` there. Declare `primitives`/`runtimes`/`formats` ONLY\n   in the C manifest, never in CMake, so the build graph can't drift from what the\n   router routes.\n7. **Android:** if the SDK loads the engine via `System.loadLibrary` +\n   `nativeRegister`, add a JNI bridge with `RAC_DEFINE_ENGINE_JNI_BRIDGE(...)`\n   (standalone per-engine `.so` — onnx, llamacpp) or\n   `RAC_DEFINE_ENGINE_JNI_BRIDGE_NO_ONLOAD(...)` (folded into a host lib that\n   already owns `JNI_OnLoad`, e.g. cloud → `librunanywhere_jni.so`) from\n   `engines/common/rac_engine_jni_bridge.h`. The JVM class-path token in the macro\n   call must match the Kotlin `*Bridge` class byte-for-byte.\n8. **iOS/WASM static hosts:** make sure the host force-loads the plugin —\n   `rac_force_load(<app> PLUGINS <name>)` in the host's CMakeLists — so the\n   static-init Registrar TU survives linker stripping.\n9. **If this engine ships an Electron/npm carrier package**, register it in\n   `scripts/validation/gates/check_plugin_natives.py`'s `REQUIRED_OPS` dict (the\n   ops-table symbol name(s) your entry TU references only on the routable\n   branch) — `package-sdk.sh` calls this automatically, so an unregistered\n   engine's stub/shell builds ship undetected. **Check which FILE actually\n   compiles `rac_plugin_entry_<name>.cpp`** before assuming the carrier\n   (`runanywhere_<name>`) is what to gate: sherpa/onnx/llamacpp compile it into\n   the carrier itself (an undefined symbol reference there is the evidence),\n   but neurt's `rac_add_engine_plugin()` call puts it on the ENGINE target\n   (`rac_backend_neurt`) instead — the carrier there is empty boilerplate with\n   zero symbols. Add the engine id to `EVIDENCE_IN_BACKEND_FILE` if it follows\n   neurt's shape; discovered by actually building neurt for real and running\n   `nm -a` on both files, not by reading the CMakeLists and assuming.\n\n## Procedure B — add a modality to an existing engine\n\nMulti-modality is the whole point of an identity-named engine. `cloud` and `neurt`\nare explicitly built for this and carry the recipe inline as a comment next to\ntheir vtable.\n\n1. Implement the new modality's op-table (e.g. a cloud TTS `g_cloud_tts_ops`,\n   typically backed by a per-modality provider adapter under `providers/`).\n2. **Fill the corresponding slot** in the engine's `rac_engine_vtable_t`\n   (`tts_ops` / `llm_ops` / `embedding_ops` / …) — every other slot must stay\n   explicit NULL.\n3. **Add the primitive** to the manifest's `primitives[]`.\n4. Update `formats[]`/`runtimes[]` only if the new modality actually needs them\n   (per THE RULE — don't declare a runtime your new op-table doesn't depend on).\n\nNo new plugin, no rename, no ABI bump — the engine already owns one\n`rac_engine_vtable_t` with all ten primitive slots; you're just filling another one.\nGrep the target engine's `rac_plugin_entry_<name>.cpp` for the phrase \"To add a\"\n— cloud and neurt both leave a comment naming exactly which slots are still free\nand what to add to `primitives[]`.\n","frontmatter":{"name":"engine-plugin-authoring","description":"Step-by-step recipe for adding a new engine plugin under runanywhere-sdks/engines/, or adding a modality (e.g. TTS) to an existing engine (llamacpp, sherpa, onnx, cloud, mlx, neurt, qhexrt). Use when the task is \"add engine X\", \"wire up backend Y\", or \"make engine Z also serve <modality>\"."},"isInternal":false,"tokens":1903,"sizeBytes":7202},{"name":"SKILL.md","path":".claude/skills/sdk-publish/SKILL.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/.claude/skills/sdk-publish/SKILL.md","title":"sdk-publish","category":"anthropic-skill","format":"markdown","content":"---\nname: sdk-publish\ndescription: Manually publish an already-tagged, already-released (non-draft GitHub Release) RunAnywhere SDK version to every package registry (npm, Maven Central, pub.dev, Swift SPM dist repo) plus macOS rcli notarization, with heavy verification that nothing fake/stubbed/broken ships.\n---\n\n# SDK Publish\n\nRepo: `~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks` (verify with `pwd`/`git remote get-url origin` — do not assume this path is still correct, checkouts move). Version source of truth: `core/VERSION`. Throughout, `$VERSION` means the bare semver being published (e.g. `0.20.24`), never prefixed with `v` except in git tags / GitHub Release names.\n\n## THIS PROCESS IS INTENTIONALLY MANUAL\n\n**`release.yml` never publishes anywhere public.** It only builds artifacts, packages them, and creates a draft/published **GitHub Release**. It does not call `npm publish`, does not upload to Maven Central, does not call `flutter pub publish`, and does not push the Swift dist repo. Verify this is still true before trusting it — greps below should all return **zero** matches (grep the whole file, not a snippet):\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks\ngrep -inE \"npm publish|publishReleasePublication|publishAllPublicationsToMavenCentral|flutter pub publish\" .github/workflows/release.yml\n```\n\nThis is a **deliberate safety gate**, not a gap. Every step below is a human (or an agent acting on a human's explicit go-ahead) pushing a button that CI intentionally never pushes. Do not propose wiring any of this into CI as a \"fix\" — that would remove the gate.\n\n## Precondition check (always run first)\n\nRefuse to proceed if the release is a draft or the asset set looks incomplete.\n\n```bash\ngh release view v$VERSION --json isDraft,tagName --jq '{isDraft, tagName}'\n# isDraft MUST be false.\n\ngh release view v$VERSION --json assets --jq '.assets[].name' | sort\n```\n\nCross-reference the listing against the `sdk-release` skill's step 8 asset-count expectations (`~/.claude/skills/sdk-release/SKILL.md`). As a second, independent check, also diff against the **previous** tagged release's asset list — same count, same name patterns, only the version token differs:\n\n```bash\ngh release view v<PREVIOUS_VERSION> --json assets --jq '.assets[].name' | sed \"s/<PREVIOUS_VERSION>/$VERSION/\" | sort >/tmp/expected.txt\ngh release view v$VERSION --json assets --jq '.assets[].name' | sort >/tmp/actual.txt\ndiff /tmp/expected.txt /tmp/actual.txt\n```\n\nAs of the last verified run (**v0.20.25, 65 assets** — up from 55 at v0.20.24 because the 5 public Electron tarballs and their sidecars joined the release; see §6), the full set is: `MANIFEST.txt`; per-platform `RACommons-{android-arm64-v8a,android-armeabi-v7a,android-x86_64,ios,linux-x86_64,web,windows-x64}-v$VERSION.{zip,tar.gz}` + `.sha256`; iOS backend zips `RABackend{LLAMACPP,MLX,NeuRT,ONNX,Sherpa}-ios-v$VERSION.zip` + `.sha256`; `RunAnywhereMLX{Metal,Resources,Runtime}-ios-v$VERSION.zip` + `.sha256`; `rcli-{linux-x86_64,macos-arm64,windows-x86_64}-v$VERSION.tar.gz`/`.zip` + `.sha256`; the 8 npm tarballs `runanywhere-{core,llamacpp,mlx,onnx,proto-ts,web,web-llamacpp,web-onnx}-$VERSION.tgz` + `.sha256`; the **5 Electron npm tarballs** `runanywhere-electron{,-llamacpp,-onnx,-sherpa,-neurt}-$VERSION.tgz` + `.sha256` (new as of v0.20.25); and `runanywhere-kotlin-maven-v$VERSION.zip` + `.sha256`. There is **no** Flutter tarball in the release (Flutter packages are staged from the native archives above, not from a dedicated release asset) and **no** `.dmg` asset in the baseline set (the macOS rcli tarball is what ships; see the notarization section for when a `.dmg` does get produced). If a `qhexrt`-named asset appears anywhere in this list, stop — that would mean `release.yml`'s own manifest assertion should have hard-failed and something is very wrong; do not publish anything from that release.\n\n## 0. MANDATORY GATE — verify the ENTIRE release before pushing anything to any registry\n\n**Run this once, in full, before the first registry push. Do not publish a single package until it passes.** Per-package spot-checks are not a substitute: they are how a bad artifact reaches a registry you cannot un-publish from. Proven end to end on v0.20.25 (2026-08-22), then independently audited and corrected: **34 payloads, 6619 files across 3 nesting levels, 282 binaries, 0 placeholder hits, 38/38 commons binaries carrying the real staging host**. The audit found 4 methodology gaps in the first pass (shallow nesting, missing `MTLB` magic, `.lib` excluded from the positive check, and a set-intersection that could pass vacuously) — every one is called out inline below, because each produced a *confident but under-covered* result.\n\n```bash\nVERSION=0.20.25                      # no leading v\nREPO=RunanywhereAI/runanywhere-sdks\nW=/tmp/relverify-$VERSION\nrm -rf \"$W\" && mkdir -p \"$W/assets\" \"$W/qhexrt\" \"$W/x\" \"$W/nested\"\n\n# (1) EVERY release asset — no -p filter, get the whole set.\ngh release download \"v$VERSION\" --repo \"$REPO\" -D \"$W/assets\"\n\n# (2) The PRIVATE QHexRT artifact is NOT a release asset — pull it from the run.\n#     Find the run that produced the release, then:\ngh run download <release-run-id> --repo \"$REPO\" --name sdk-electron-qhexrt-private --dir \"$W/qhexrt\"\n```\n\n### (3) Checksums — `cd` into the directory first\n\nSidecars contain a **bare basename**, so `shasum -c` fails with a misleading \"FAILED open or read\" if run from anywhere else. This will make every artifact look broken when nothing is wrong:\n\n```bash\ncd \"$W/assets\"\nok=0; bad=0\nfor s in *.sha256; do\n  if shasum -a 256 -c \"$s\" >/dev/null 2>&1; then ok=$((ok+1)); else echo \"MISMATCH: $s\"; bad=$((bad+1)); fi\ndone\necho \"verified=$ok mismatched=$bad\"      # bad MUST be 0\n(cd \"$W/qhexrt\" && shasum -a 256 -c ./*.sha256)\n```\n\n### (4) Extract everything — INCLUDING nested archives\n\n**The gotcha that matters:** `runanywhere-kotlin-maven-v*.zip` contains `.aar` files, and the `.aar`s are what hold the Android `.so` binaries. A one-level extraction reports \"0 binaries\" for the Maven bundle and every subsequent scan passes **vacuously**.\n\n**Recurse until the tree is dry — TWO levels are required here, not one.** Each `.aar` also contains a `classes.jar`, holding ~2027 `.class` files that are doubly unreachable: inside an unopened jar *and* excluded by the `.class` suffix rule most magic-byte scanners use. Level 2 adds **2033 files** (0 placeholder hits on v0.20.25); level 3 is empty. Loop until a pass finds no new archives rather than hardcoding a depth.\n\n```bash\ncd \"$W/assets\"\nfor f in *; do\n  case \"$f\" in *.sha256|MANIFEST.txt) continue;; esac\n  d=\"$W/x/$f\"; mkdir -p \"$d\"\n  case \"$f\" in\n    *.zip)              unzip -qq -o \"$f\" -d \"$d\" || echo \"EXTRACT FAILED: $f\" ;;\n    *.tar.gz|*.tgz)     tar -xzf \"$f\" -C \"$d\"     || echo \"EXTRACT FAILED: $f\" ;;\n    *) echo \"UNHANDLED TYPE: $f\" ;;\n  esac\ndone\nd=\"$W/x/$(basename \"$W\"/qhexrt/*.tgz)\"; mkdir -p \"$d\"; tar -xzf \"$W\"/qhexrt/*.tgz -C \"$d\"\n\n# nested pass\ncd \"$W/x\"\nfind . -type f \\( -name '*.aar' -o -name '*.jar' -o -name '*.zip' -o -name '*.tgz' -o -name '*.tar.gz' \\) \\\n  | sed 's|^\\./||' | while IFS= read -r a; do\n    dd=\"$W/nested/$(printf '%s' \"$a\" | tr '/' '_')\"; mkdir -p \"$dd\"\n    case \"$a\" in *.aar|*.jar|*.zip) unzip -qq -o \"$a\" -d \"$dd\";; *) tar -xzf \"$a\" -C \"$dd\";; esac\n  done\n```\n\n**Prove extraction actually happened** — an empty dir makes \"0 hits\" meaningless:\n\n```bash\nfor d in \"$W\"/x/*/; do\n  n=$(find \"$d\" -type f | wc -l)\n  [ \"$n\" -lt 2 ] && echo \"SUSPICIOUSLY EMPTY: $d ($n files)\"\ndone\n```\n\n### (5) Placeholder scan — EVERY file, not just binaries\n\nThe staging URL can also live in JS/JSON/TS config, so scan by content across all file types, and additionally scan the raw compressed streams in case a member never got extracted:\n\n```bash\nfind \"$W/x\" \"$W/nested\" -type f | wc -l                                     # total files covered\ngrep -rla \"YOUR_STAGING_BASE_URL\" \"$W/x\" \"$W/nested\" | tee /tmp/ph.txt | wc -l   # MUST be 0\nfor f in \"$W\"/assets/*.tgz \"$W\"/assets/*.tar.gz \"$W\"/qhexrt/*.tgz; do\n  gzip -dc \"$f\" 2>/dev/null | grep -qa \"YOUR_STAGING_BASE_URL\" && echo \"RAW HIT: $f\"\ndone\n```\n\nFind binaries by **magic bytes, not extension** (an unsuffixed framework binary like `RACommons.framework/RACommons` has no extension and a name-based `find` misses it). Signatures to cover:\n\n| Magic | Kind | v0.20.25 count |\n|---|---|---|\n| `7f454c46` | ELF | 177 |\n| `!<arch>` (`213c617263683e0a`) | static `.a` **and `.lib`** | 41 |\n| `MZ` | PE | 29 |\n| `cffaedfe` / `cefaedfe` | Mach-O | 19 |\n| `00 61 73 6d` | wasm | 10 |\n| **`4d544c42` (`MTLB`)** | **Metal library** — easy to miss | 5 |\n| `cafebabe` / `bebafeca` | fat Mach-O (exclude `.jar`/`.class`) | 1 |\n\n**Total: 282 binaries.** An earlier pass reported 277 because `MTLB` was absent from the signature list — `.metallib` files were invisible to the binary scan (they were still covered by the all-file `grep` in step 5, so the conclusion held, but the count was wrong). If your total comes in materially under ~282 for a comparable release, your detection is leaking.\n\n### (6) POSITIVE staging-URL check — absence of the placeholder is NOT sufficient\n\nA build with an **empty** URL baked in passes a placeholder-only check. `check_plugin_natives.py` only tests absence, so add the positive assertion yourself.\n\nTwo traps that produced false confidence on the v0.20.25 pass, both worth avoiding:\n\n- **Include `.lib`.** A `MUST_HAVE` regex of `rac_commons\\.(dylib|so|dll|a)$` silently skips\n  `RACommons-windows-x64/x64/rac_commons.lib` — an **89 MB** binary that then gets no positive\n  check at all. Match `(dylib|so|dll|a|lib)` plus `\\.wasm$`. Correct coverage is **38** binaries,\n  not 37.\n- **Assert per binary, not by set-intersection.** Intersecting \"non-public origins across all\n  files\" yields more than one origin here (`hf.co` *and* the staging host), so a binary carrying\n  only `hf.co` would pass **vacuously**. Check each binary individually for the staging host.\n\n```bash\n# v0.20.25 baseline: 38 commons/wasm/lib binaries; 0 missing the staging host.\n```\n\nNever print the URL — it is a repo secret. Report a truncated SHA-256 fingerprint, or just assert presence/absence counts.\n\n### (7) Routability gate over the whole tree\n\n```bash\npython3 scripts/validation/gates/check_plugin_natives.py \"$W/x\" \"$W/nested\" > /tmp/gate.txt 2>&1\ngrep -cE '^  ok ' /tmp/gate.txt; grep -E '^  FAIL' /tmp/gate.txt\n```\n\nv0.20.25 baseline: **77 ok, 1 FAIL**. The single expected FAIL is\n`RACommons-linux-x86_64/lib/librunanywhere_sherpa.so` — see the known-exception note below. **Any other FAIL blocks the publish.**\n\n### (8) Known exception — Linux sherpa is a non-routable shell (PRE-EXISTING, not a regression)\n\n`RACommons-linux-x86_64-v*.tar.gz` ships **no** `libsherpa-onnx-c-api.so` at all and its `librac_backend_sherpa.so` is a ~156 KB shell (~3 sherpa strings, vs ~31 MB / ~192 for a healthy one on Electron macOS) — the engine is not linked in, so the carrier cannot reference `g_sherpa_{stt,tts,vad}_ops` and **STT/TTS/VAD cannot work from that bundle**. Android's carriers pass on all 3 ABIs, so this is Linux-only.\n\n**Compare the CARRIER, not the backend.** The gate FAILs on `librunanywhere_sherpa.so` (the carrier); citing `librac_backend_sherpa.so` as your evidence is comparing a *different file* and is not proof. The carrier is byte-identical across releases, which is the real proof:\n\n```bash\ngh release download \"v<PREV>\" --repo \"$REPO\" -p 'RACommons-linux-x86_64-v<PREV>.tar.gz' -D /tmp/prev\ntar -xzf /tmp/prev/*.tar.gz -C /tmp/prev\nshasum -a 256 /tmp/prev/lib/librunanywhere_sherpa.so \"$W\"/x/RACommons-linux-*/lib/librunanywhere_sherpa.so\n# v0.20.24 and v0.20.25 both: size=15712  sha256=28ef16015b61da86...\n\n# strongest check — run the gate on the PREVIOUS bundle and confirm the identical FAIL:\npython3 scripts/validation/gates/check_plugin_natives.py /tmp/prev\n```\n\nIf it ever regresses *further* (e.g. Android or macOS sherpa also goes hollow), stop and treat it as a blocking defect.\n\n### (9) Account for every payload explicitly\n\nPrint a per-artifact table (checksum verdict, binary count, placeholder hits) and confirm the row count equals the payload count. Artifacts that legitimately contain **no** native binaries, and why — do not let these pass silently unexamined:\n\n| Artifact | Why it has no binaries |\n|---|---|\n| `RunAnywhereMLXResources-ios-*.zip` | 25 third-party license/notice text files |\n| `runanywhere-kotlin-maven-v*.zip` | binaries live in the **nested** `.aar`s (step 4) |\n| `runanywhere-proto-ts-*.tgz` | pure TS/JS (69 `.ts` + 69 `.js`) |\n\nEverything else must have a non-zero, plausible binary count. A platform suddenly shipping fewer binaries than the previous release is a regression signal worth chasing before publishing.\n\n### (10) Blind spot to be aware of — Windows x64 has NO routability coverage\n\n`RACommons-windows-x64-v*.zip` ships **only** a static `rac_commons.lib` (~89 MB) plus 152 `.h` / 11 `.hpp` headers — **zero backend DLLs**, where Linux/Android/Web each ship 4–5 backend carriers. That is consistent with a static-embedding bundle (consumers link the engines in themselves), but the consequence matters: **no gate can detect a missing or hollow Windows backend**, because there is no carrier to inspect. `check_plugin_natives.py` therefore reports nothing at all for this artifact rather than reporting health.\n\nWindows x64 is also *advisory* in `release.yml` (its absence does not fail the release). So a Windows regression can pass every automated check. If Windows correctness matters for a given release, verify it by actually running something on the real Windows box (see §Electron for how to reach it) — do not infer it from a green pipeline. Worth a deliberate confirmation with the repo owner that the DLL-free shape is intentional.\n\n---\n\n## General verification discipline (every artifact, every registry)\n\nApply all three checks below to **every** file you're about to publish, before AND after the publish call. Never trust a copy cached locally earlier in the session — re-download.\n\n**(a) Download + verify the sha256 sidecar yourself:**\n\n```bash\ngh release download v$VERSION -p '<file>' -p '<file>.sha256' -D /tmp/sdk-publish-$VERSION\ncd /tmp/sdk-publish-$VERSION && shasum -a 256 -c '<file>.sha256'\n```\n\n**(b) Inspect the actual file listing inside every tarball/AAR/zip** — never assume from the filename alone:\n\n```bash\nnpm pack <tgz-or-package-dir> --dry-run     # npm packages: prints the exact file list it would publish\nunzip -l  <file.zip>                        # AARs, Maven repo zip, native archives\ntar -tzf  <file.tar.gz>                     # rcli, RACommons tarballs\n```\n\nSanity-check what comes back:\n- No suspiciously-small or empty binaries where a real native library (`.so`, `.dylib`, `.a`, `.dll`) should be — a stub or a zero-byte placeholder means the build silently failed upstream.\n- No leftover `workspace:*` or `file:` dependency specs in any `package.json` inside the tarball — those only resolve inside this monorepo and break for a real external installer.\n- Version strings inside every manifest (`package.json` `version`, AAR `pom.xml` `<version>`, `pubspec.yaml` `version:`) **exactly** match `$VERSION`.\n- **Hard rule, no exceptions:** grep the full file listing for `qhexrt` (any case) and for QNN binary names (`libQnnHtp`, `QnnHtp*.dll`, `rac_commons.dll` bundled with QHexRT, `runanywhere_qhexrt`). If any hit appears in a package meant to be public, stop and do not publish that artifact.\n\n**(c) Confirm baked-in runtime config** (e.g. a staging-vs-prod backend base URL compiled into `rac_commons` at build time) points at the real intended endpoint. This is designed to fail closed if misconfigured — a build that completed and produces working requests against prod is itself decent evidence — but if you have any doubt, `strings <binary> | grep -i \"api\\.\\|staging\\.\"` and eyeball it.\n\n## 1. npm — 8 packages, order matters\n\nPackages: 4 Web (`@runanywhere/proto-ts`, `@runanywhere/web`, `@runanywhere/web-llamacpp`, `@runanywhere/web-onnx`) + 4 React Native (`@runanywhere/core`, `@runanywhere/llamacpp`, `@runanywhere/mlx`, `@runanywhere/onnx`). Never `@runanywhere/qhexrt` (RN) — it exists in `bindings/react-native/packages/qhexrt/` but is excluded from the public npm train by the same policy as Maven/pub.dev QHexRT.\n\n**Publish `@runanywhere/proto-ts` FIRST.** `@runanywhere/web-onnx` deliberately does not bundle its own `proto-ts` copy and needs it resolvable from the live registry for real consumers; publish `proto-ts` before it or a fresh `npm install @runanywhere/web-onnx` fails to resolve.\n\n```bash\ngh release download v$VERSION -p 'runanywhere-proto-ts-*.tgz*' -p 'runanywhere-web-*.tgz*' \\\n  -p 'runanywhere-core-*.tgz*' -p 'runanywhere-llamacpp-*.tgz*' -p 'runanywhere-mlx-*.tgz*' -p 'runanywhere-onnx-*.tgz*' \\\n  -D /tmp/sdk-publish-$VERSION\ncd /tmp/sdk-publish-$VERSION\nfor f in *.sha256; do shasum -a 256 -c \"$f\"; done\n\nnpm publish runanywhere-proto-ts-$VERSION.tgz --access public   # FIRST, always\nnpm publish runanywhere-web-$VERSION.tgz --access public\nnpm publish runanywhere-web-llamacpp-$VERSION.tgz --access public\nnpm publish runanywhere-web-onnx-$VERSION.tgz --access public\nnpm publish runanywhere-core-$VERSION.tgz --access public\nnpm publish runanywhere-llamacpp-$VERSION.tgz --access public\nnpm publish runanywhere-mlx-$VERSION.tgz --access public\nnpm publish runanywhere-onnx-$VERSION.tgz --access public\n```\n\n`npm whoami` should already show a logged-in account; if it errors, stop and tell the human rather than trying to log in non-interactively.\n\n**GOTCHA — large tarballs can silently die mid-upload.** `@runanywhere/mlx` (~50MB) and `@runanywhere/web-onnx` (unpacks to 300+MB) can exceed a short default shell/tool timeout mid-upload and get killed with no clean error. Use an explicit long timeout (5 minutes / 300000ms) on each publish call.\n\n**GOTCHA — verify via the registry API, not the npm CLI.** `npm view` can lag the CLI's own cache by several seconds right after a fresh publish and falsely report \"not found\". After **every** publish:\n\n```bash\ncurl -s \"https://registry.npmjs.org/@runanywhere/<pkg>\" | python3 -c \"import json,sys;print(json.load(sys.stdin)['dist-tags'])\"\n```\nConfirm `latest` equals `$VERSION`.\n\n## 2. Maven Central — 3 AARs only\n\n`io.github.sanchitmonga22:{runanywhere-sdk,runanywhere-llamacpp,runanywhere-onnx}`. **Never `runanywhere-qhexrt-android`** — it was published through 0.20.19 under an earlier, since-reversed policy and is now permanently excluded; `release.yml`'s manifest assertion hard-fails if it reappears in the public asset set.\n\nFull step-by-step lives in `bindings/kotlin/docs/KOTLIN_MAVEN_CENTRAL_PUBLISHING.md`, section **\"Option A: Publish with released native archives\"** (line ~108 as of this writing — the doc has been corrected mid-release before, so re-read it fresh rather than trusting memory of its contents). Key points, condensed:\n\n1. Download + verify the 3 `RACommons-android-{arm64-v8a,armeabi-v7a,x86_64}-v$VERSION.zip` from the release (sha256 sidecars, per the general discipline above).\n2. Stage: `bash bindings/kotlin/scripts/package-sdk.sh --mode local --natives-from <dir-with-the-3-zips>`. This also produces a local Maven-repo zip — diff its size against `runanywhere-kotlin-maven-v$VERSION.zip` already attached to the GitHub Release as a cheap reproducibility check.\n3. Before uploading, confirm the OSSRH compatibility service has no dangling open repository for this namespace:\n   ```bash\n   CENTRAL_BEARER=\"$(printf '%s:%s' \"$MAVEN_CENTRAL_USERNAME\" \"$MAVEN_CENTRAL_PASSWORD\" | base64 | tr -d '\\r\\n')\"\n   curl -s -H \"Authorization: Bearer $CENTRAL_BEARER\" \\\n     'https://ossrh-staging-api.central.sonatype.com/manual/search/repositories?ip=client&profile_id=io.github.sanchitmonga22'\n   ```\n   Resolve any stale repository per the doc's troubleshooting table before continuing.\n4. Source `MAVEN_CENTRAL_USERNAME`/`MAVEN_CENTRAL_PASSWORD` from `~/.gradle/gradle.properties` (`mavenCentral.username`/`mavenCentral.password` — confirmed present on this machine; never print the values) and run, from `bindings/kotlin/`:\n   ```bash\n   ./gradlew clean \\\n     :publishReleasePublicationToMavenCentralRepository \\\n     :modules:runanywhere-core-llamacpp:publishReleasePublicationToMavenCentralRepository \\\n     :modules:runanywhere-core-onnx:publishReleasePublicationToMavenCentralRepository \\\n     -Prunanywhere.useLocalNatives=true -x buildLocalJniLibs --no-daemon\n   ```\n   GPG signing (`signing.gnupg.keyName`/`signing.gnupg.passphrase` in the same properties file, key id `CC377A9928C7BB18` — confirmed importable via `gpg --list-secret-keys --keyid-format LONG` on this machine) is read from the same properties file automatically; you do not pass it separately.\n5. Transfer to Central Portal:\n   ```bash\n   curl --fail --request POST -H \"Authorization: Bearer $CENTRAL_BEARER\" \\\n     'https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/io.github.sanchitmonga22?publishing_type=automatic'\n   ```\n   Then find the resulting `portal_deployment_id` — expect **exactly one** fresh repository; if the count isn't 1, stop and investigate rather than guessing which one is yours (see the doc for the exact `jq` filter).\n6. Poll `https://central.sonatype.com/api/v1/publisher/status?id=<id>` (POST, same bearer) until `deploymentState` is `PUBLISHED`. Automatic deployments normally go `PENDING -> VALIDATING -> PUBLISHING -> PUBLISHED` within a few minutes. **If it lands on `VALIDATED` instead of proceeding, that means Sonatype is waiting for a manual click in the Central Portal UI** — stop and tell the human; do not loop forever assuming it resolves itself. On `FAILED`, print `.errors` and stop.\n7. Verify propagation (allow 10-30 minutes for CDN propagation even after `PUBLISHED`):\n   ```bash\n   for a in runanywhere-sdk runanywhere-llamacpp runanywhere-onnx; do\n     echo \"$a: $(curl -s -o /dev/null -w '%{http_code}' \"https://repo1.maven.org/maven2/io/github/sanchitmonga22/$a/$VERSION/$a-$VERSION.pom\")\"\n   done\n   ```\n   Expect `200` for all three.\n\n## 3. pub.dev (Flutter)\n\n**Verify the current package set fresh — do not trust a hardcoded count.** As of this writing the real, non-QHexRT Flutter package set is **4**, not 3: `runanywhere`, `runanywhere_llamacpp`, `runanywhere_onnx`, and `runanywhere_mlx` (mlx was added after some earlier documentation/memory of \"3 packages\" was written — that assumption is stale; `runanywhere_mlx` is a real, already-published-on-pub.dev package, not new/WIP). Enumerate it yourself instead of trusting this list verbatim next time:\n```bash\nfind bindings/flutter/packages -maxdepth 1 -type d -not -iname '*qhexrt*' -not -path '*/packages'\n```\n`runanywhere_qhexrt` exists in that directory too — never publish it (same exclusion policy as Maven).\n\nStage the released iOS xcframeworks + Android `.so` into each package with the Flutter equivalent of `package-sdk.sh` (melos-based monorepo, confirmed at `bindings/flutter/scripts/package-sdk.sh`):\n\n```bash\nbash bindings/flutter/scripts/package-sdk.sh --mode local --natives-from <dir-with-xcframeworks-and-android-.so>\n```\n\nThis script already runs `flutter pub publish --dry-run` internally per package as part of its own validation — but run it again yourself, per package, from each package directory, and **read the output for real, not just the exit code**:\n\n```bash\ncd bindings/flutter/packages/runanywhere && flutter pub publish --dry-run\ncd ../runanywhere_llamacpp && flutter pub publish --dry-run\ncd ../runanywhere_onnx && flutter pub publish --dry-run\ncd ../runanywhere_mlx && flutter pub publish --dry-run\n```\n\nExpect zero warnings except a benign version-gap hint. Only then run the real publish, same order (core first, since the others depend on it):\n\n```bash\ncd bindings/flutter/packages/runanywhere && flutter pub publish\ncd ../runanywhere_llamacpp && flutter pub publish\ncd ../runanywhere_onnx && flutter pub publish\ncd ../runanywhere_mlx && flutter pub publish\n```\n\n**Credential note:** pub.dev OAuth is normally cached at `~/Library/Application Support/dart/pub-credentials.json` on macOS (confirmed present on this machine). If it's missing, expired, or `flutter pub publish` prompts for an interactive browser login, **STOP and tell the human** — you cannot complete a fresh Google OAuth flow non-interactively, and forcing/guessing here is not appropriate.\n\nVerify afterward against the live API (not a cached page):\n```bash\nfor p in runanywhere runanywhere_llamacpp runanywhere_onnx runanywhere_mlx; do\n  echo \"$p: $(curl -s \"https://pub.dev/api/packages/$p\" | python3 -c \"import json,sys;print(json.load(sys.stdin)['latest']['version'])\")\"\ndone\n```\n\n## 4. Swift SPM distribution repo (`RunanywhereAI/runanywhere-swift`)\n\nA separate, **generated-only** repo mirroring `bindings/swift` (Package.swift + Sources/ + LICENSE + README) so SwiftPM consumers using `from: \"<version>\"` don't clone the ~340MB monorepo.\n\n**GOTCHA:** this machine may have local directories literally named `runanywhere-swift` that are just checkouts of the **monorepo's** remote, not the distribution repo. Verify before trusting any existing local directory by name:\n```bash\ngit -C <candidate-dir> remote get-url origin\n```\nMust show `RunanywhereAI/runanywhere-swift`, not `runanywhere-sdks`. Always clone fresh instead of guessing:\n\n```bash\ngit clone https://github.com/RunanywhereAI/runanywhere-swift.git /tmp/ra-swift\n\nbindings/swift/scripts/sync-dist-repo.sh \\\n  --zips <exact-verified-Apple-archive-dir-from-the-release> --tag /tmp/ra-swift\n\nRUNANYWHERE_SWIFT_DIST_REPO=/tmp/ra-swift \\\n  bash scripts/validation/gates/check_swift_dist_repo_sync.sh\n\ngit -C /tmp/ra-swift push origin main --follow-tags\n```\n\nThe `--zips` dir must contain the verified (sha256-checked) release archives — `RACommons-ios-v$VERSION.zip`, `RABackend{LLAMACPP,MLX,NeuRT,ONNX,Sherpa}-ios-v$VERSION.zip`, `RunAnywhereMLX{Metal,Resources,Runtime}-ios-v$VERSION.zip` — downloaded and checked exactly per the general verification discipline, not re-used from an earlier, untrusted local copy.\n\n**The tag on `runanywhere-swift` is bare semver, no `v` prefix** (SwiftPM's `from:` needs that) — do not tag it `v$VERSION` by mistake; `sync-dist-repo.sh --tag` handles this correctly, but double check the pushed tag with `git -C /tmp/ra-swift tag --points-at HEAD` before walking away.\n\nThis is enforced, not just documented: once `v$VERSION` is tagged on `runanywhere-sdks`, `check_swift_dist_repo_sync.sh` fails every future PR on the monorepo until `runanywhere-swift` carries the matching bare-semver tag — so don't let this lag behind the other registries.\n\n**GATE GAP — `check_swift_dist_repo_sync.sh` does NOT catch dependency drift, only checksum/version-string drift.** `runanywhere-swift`'s `Package.swift` is manually maintained end to end — `sync-dist-repo.sh` only ever touches `sdkVersion` and the binaryTarget checksums, never the `dependencies:` block (mlx-swift, mlx-swift-lm, mlx-audio-swift, swift-crypto, etc.). If root `runanywhere-sdks/Package.swift` bumps one of those (e.g. a new fork tag), a human has to remember to mirror it into the dist repo by hand, and nothing fails CI if they forget. This exact drift shipped once (2026-08-17): the dist repo pointed `mlx-swift-lm` at unforked `ml-explore/mlx-swift-lm` instead of the `RunanywhereAI` fork, silently breaking every iOS/macOS MLX consumer of that tag until caught by an actual build failure ~40 minutes later. Before trusting a freshly-cut dist-repo tag: diff every `.package(url:` line between root `Package.swift` and the dist repo's `Package.swift` (`grep -n '\\.package(url:' Package.swift` on both sides), and actually build a real product that exercises the dependency you're worried about (e.g. `cd /tmp/ra-swift && swift build --product RunAnywhereMLX`) — don't rely on the sync gate alone. If you catch drift on a tag that's only minutes old with no evidence of external consumption, fixing it in place and force-moving the tag (`git tag -f $VERSION && git push origin $VERSION --force`) is the correct call, not a shortcut — a known-broken immutable tag helps no one.\n\n**If you force-move a tag, SwiftPM/Xcode consumers cache the OLD commit in several places — clearing just `Package.resolved` is not enough.** Verifying your own fix (or having a downstream consumer verify it) can silently keep failing even after the fix is correctly pushed, because: (1) any consumer repo's own tracked `*.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved` pins the exact old resolution and `xcodebuild -resolvePackageDependencies` respects it rather than re-resolving; (2) `~/Library/Caches/org.swift.swiftpm/repositories/<pkg>-<hash>/` is a global shared clone cache keyed by repo, oblivious to a moved tag; (3) `~/Library/org.swift.swiftpm/security/fingerprints/<pkg>-<hash>.json` records the fingerprint SwiftPM saw for that tag and can silently keep using it after a mismatch instead of failing loud. To actually re-resolve after moving a tag: delete the consumer's tracked `Package.resolved`, `rm -rf .build .swiftpm` and any `~/Library/Developer/Xcode/DerivedData/<Scheme>-*`, AND clear the matching entries under both global `org.swift.swiftpm` cache directories for every affected package identity — then resolve fresh.\n\n## 5. macOS rcli notarization\n\nCI (`native_rcli_macos` in `release.yml`) usually already produces a notarized, stapled artifact when Developer ID + notary secrets are configured in repo secrets. **Check the release assets first** before assuming you need to notarize anything yourself locally:\n\n```bash\ngh release view v$VERSION --json assets --jq '.assets[].name' | grep -iE 'rcli-macos|\\.dmg'\n```\nBaseline expectation is `rcli-macos-arm64-v$VERSION.tar.gz` (+ `.sha256`); a `.dmg` only appears when `RCLI_MACOS_FULL_RELEASE=1` was set for that build (see below). If a plain tarball is already present and its contents pass the general verification discipline, there is usually nothing further to do here.\n\n**If you do need to notarize locally**, everything lives in `rcli/scripts/package-rcli.sh` (read its header comment — it documents its own contract in detail):\n\n- One-time setup, check before redoing: `xcrun notarytool history --keychain-profile runanywhere-notary` (confirmed configured on this machine — a fresh submission history returns without error). If missing: `xcrun notarytool store-credentials \"runanywhere-notary\" --apple-id <email> --team-id <team> --password <app-specific-password>`.\n- Codesign identity: `security find-identity -v -p codesigning | grep \"Developer ID Application\"` — confirmed present on this machine as `\"Developer ID Application: RunAnywhere, Inc (<TEAM_ID>)\"`. Grep for it fresh rather than hardcoding the team-id suffix, it can rotate.\n- Invocation contract (env vars, from the script header): `RCLI_CODESIGN_IDENTITY`, `RCLI_MACOS_NOTARIZE=1`, `RCLI_MACOS_FULL_RELEASE=1`, `RCLI_MACOS_SWIFT_BIN_DIR=<output of build-mlx-cli.sh>`. `RCLI_MACOS_FULL_RELEASE=1` is what additionally stages `mlx.metallib` and SwiftPM resource bundles and is what causes a notarized, stapled `.dmg` to be emitted alongside the Homebrew-style tarball. `RCLI_CODESIGN_KEYCHAIN` / `RCLI_NOTARYTOOL_KEYCHAIN` matter only if the identity/profile live in a non-default keychain.\n- Call shape: `rcli/scripts/package-rcli.sh <build-dir> macos-arm64` with the env vars above exported.\n\n## 6. Electron — build AND packaging are now in `release.yml`; publish is still manual\n\n**Reversing the earlier \"deliberately separate\" note**: as of `fix/electron-cicd-staging-url-npu-ane` (2026-08-21), the repo owner asked for Electron to produce ready-to-publish artifacts through the real release train like every other SDK, so `release.yml` now has `native_electron` (invokes `.github/workflows/electron-native-package.yml` as a reusable `workflow_call`) and `sdk_electron` (downloads its `electron-packaged-tarballs` artifact, runs `scripts/release/prepublish_check.py` on every tarball, checksums them, and splits public vs. QHexRT-private). `publish` hard-gates on `sdk_electron` succeeding. `@runanywhere/electron`, `-llamacpp`, `-onnx`, `-sherpa`, `-neurt` land as real, `.sha256`-sidecarred GitHub Release assets, same as the Web/RN npm tarballs. `@runanywhere/electron-qhexrt` still never becomes a public Release asset (policy below) — it's produced and packaged every release, but only reachable as the `sdk-electron-qhexrt-private` workflow artifact on that run, for a human to `npm publish` by hand. Electron is **not** WIP on a branch anymore: the old root `AGENTS.md` \"Active issues\" section describing `smonga/electron_upgrade` as in-progress is gone entirely as of this checkout — `bindings/electron/` is a fully merged, real SDK on `main`. Re-check that section is still absent before trusting this note.\n\n`.github/workflows/electron-native-package.yml` itself is unchanged in what it builds: every backend for real (llamacpp/onnx/sherpa/neurt on their real platforms, qhexrt on a self-hosted Windows ARM64 runner) with the real `STAGING_BASE_URL` baked in, gated by `check_plugin_natives.py` (which fails closed on the staging-URL placeholder — the exact bug that shipped through `0.20.24`). What's new is that it's also invocable via `workflow_call`, and that `native_win_x64_and_package` now tolerates a failed/skipped QHexRT lane (self-hosted, single machine) rather than losing the other five real packages to one runner's outage. **`release.yml` itself still does not `npm publish` anything** — it only produces artifacts (Release assets for the public five, a workflow artifact for QHexRT) — same \"build automated, publish manual\" shape as every other platform. See `thoughts/shared/plans/electron_cicd_staging_url_and_npu_ane_pipeline.md` for the full history.\n\n**PROVEN END TO END on v0.20.25 (2026-08-22)** — the first release where this actually ran. Final state: 65 release assets, all 5 public Electron tarballs present with `.sha256` sidecars, zero `qhexrt`-named assets. Verified by downloading the *published* tarballs and running the repo's own gate against them:\n\n```bash\ngh release download v<version> --repo RunanywhereAI/runanywhere-sdks -p 'runanywhere-electron*.tgz' -D /tmp/verify\npython3 scripts/validation/gates/check_plugin_natives.py --expect-version <version> \\\n    $(for t in /tmp/verify/*.tgz; do printf -- '--tarball %s ' \"$t\"; done)\n# => All 20 check(s) passed: plugin natives are routable and no staging-URL placeholder was found.\n```\n\nThat single command asserts all three things you actually care about, per binary: the real `STAGING_BASE_URL` is baked in (no `YOUR_STAGING_BASE_URL`), `rac_commons` embeds the right version, and every backend is genuinely **routable** (`g_llamacpp_ops`, `g_sherpa_stt_ops`, `g_neurt_llm_ops`, `g_onnx_embeddings_ops`, …) rather than a same-sized shell. **Run it before every Electron `npm publish`** — it is the cheapest defense against exactly the class of defect that shipped `electron-sherpa` 0.20.17's NULL-vtable carrier. Expected shipped layout: entry package = `darwin-arm64` + `win32-x64` + `win32-arm64`; llamacpp/onnx/sherpa = `darwin-arm64` + `win32-x64`; neurt = `darwin-arm64` only; qhexrt = `win32-arm64` only.\n\nWhere to get the QHexRT tarball for its manual publish (it is deliberately NOT a release asset):\n\n```bash\ngh run download <release-run-id> --repo RunanywhereAI/runanywhere-sdks \\\n    --name sdk-electron-qhexrt-private --dir /tmp/qhexrt\n```\n\nIf asked to actually publish an Electron package (still a real, separate, manual step — this skill's general discipline in §0/§General verification still applies in full):\n- Involve the Windows box for real Windows-native verification before publishing anything Windows-targeted. The team keeps a **Windows ARM64 machine reachable over SSH**; its hostname, user, key, and mesh-VPN node name are operator-specific and deliberately not recorded in this repo — read the Windows/ARM64 host entry out of the operator's `~/.ssh/config` and use that alias. If there is no such entry, the box is not configured for you: say so and ask, rather than guessing at a hostname. Check reachability **first** by asking the mesh-VPN client whether the node is up (e.g. `tailscale status`, matching on the host entry's `HostName`) — if it reports `offline, last seen ...`, the box is asleep, which is not a configuration problem: ask the human to wake it, do not debug it as broken. Only then `ssh -o ConnectTimeout=8 \"$WIN_BOX\" \"whoami\"`. That same machine is **also** registered as a GitHub Actions self-hosted runner (labels `[self-hosted, Windows, ARM64, qhexrt]`) running as a Windows service — a queued `native_win_arm64_qhexrt` job on it means the machine is busy; check `gh api repos/RunanywhereAI/runanywhere-sdks/actions/runners` for the matching runner's `busy`/`status` before assuming it's free for manual SSH work, and vice versa.\n- **`@runanywhere/electron-qhexrt` being public on npm is a CONFIRMED, settled policy decision** (repo owner, 2026-08-20) — it contains real Hexagon NPU binaries (`libQnnHtpV81Skel.so`, `QnnHtp*.dll`, `rac_commons.dll`, `runanywhere_qhexrt.dll`) on purpose. The QHexRT-stays-private policy enforced everywhere else in this skill is specifically about the **GitHub-Release-side** asset set (Android etc.), a different channel — not a contradiction. Stop treating this as an unresolved item to flag; only re-raise it if you find evidence the policy changed.\n\n## 7. Final sanity pass\n\nAfter every registry is done, re-view each package's public listing/API and compare against the previous version — a big unexplained drop in file count or package size is a red flag to investigate before calling the release fully published:\n\n```bash\n# npm\nfor p in proto-ts web web-llamacpp web-onnx core llamacpp mlx onnx; do\n  npm pack @runanywhere/$p@$VERSION --dry-run 2>&1 | tail -5\ndone\n\n# Maven — check module metadata / POM sizes are non-trivial, not near-empty\nfor a in runanywhere-sdk runanywhere-llamacpp runanywhere-onnx; do\n  curl -s -o /dev/null -w \"%{size_download} bytes  $a\\n\" \\\n    \"https://repo1.maven.org/maven2/io/github/sanchitmonga22/$a/$VERSION/$a-$VERSION.aar\"\ndone\n\n# pub.dev\nfor p in runanywhere runanywhere_llamacpp runanywhere_onnx runanywhere_mlx; do\n  curl -s \"https://pub.dev/api/packages/$p\" | python3 -c \"import json,sys;d=json.load(sys.stdin);print('$p', d['latest']['version'])\"\ndone\n```\n\nOnly after this pass — every registry shows `$VERSION`, every artifact's contents check out, and nothing regressed in size/file-count versus the previous release — is the release fully published. Do not skip this step just because each individual publish call reported success; a successful upload is not the same claim as \"the right bytes are now live.\"\n","frontmatter":{"name":"sdk-publish","description":"Manually publish an already-tagged, already-released (non-draft GitHub Release) RunAnywhere SDK version to every package registry (npm, Maven Central, pub.dev, Swift SPM dist repo) plus macOS rcli notarization, with heavy verification that nothing fake/stubbed/broken ships."},"isInternal":false,"tokens":10460,"sizeBytes":39036},{"name":"SKILL.md","path":".claude/skills/sdk-release/SKILL.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/.claude/skills/sdk-release/SKILL.md","title":"sdk-release","category":"anthropic-skill","format":"markdown","content":"---\nname: sdk-release\ndescription: Cut a new RunAnywhere SDK release end-to-end in RunanywhereAI/runanywhere-sdks — version bump through a fully green, tagged, drafted-then-published GitHub Release. Stops BEFORE publishing to package registries (npm/Maven/pub.dev/SwiftPM dist) — that is the separate \"sdk-publish\" skill, invoked as the last step here.\n---\n\n# SDK Release (runanywhere-sdks)\n\nRepo: `~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks` (verify with `pwd`/`ls` —\nthis is a workspace subfolder, not the repo root of the whole Qualcomm/ tree, and paths\ndrift; re-check script paths below still exist before trusting them blindly).\n\nThis is a **runbook**, not a description. Follow the steps in order. Every `gh`/`git`\ncommand below is meant to be run close to verbatim — adjust only the version/PR/run-id\nvalues for the release you're actually cutting.\n\n**THIS SKILL HAS BEEN SKIPPED THREE TIMES IN A ROW (v0.20.29, v0.20.30, v0.20.31),\nproducing the IDENTICAL failure each time, DESPITE step 2/3 below documenting the exact\nfix since the v0.20.25/v0.20.29 incidents.** All three times, an agent ran\n`sync-versions.sh`, opened the PR, merged it, and pushed the tag directly — skipping the\n\"dispatch a release candidate BEFORE merging, sync checksums from it, THEN merge\" sequence\nin steps 2-3 — usually because the version bump was one step inside a longer autonomous\nchain (a downstream repo re-pin, a multi-repo release cascade) and felt like \"just a\nversion bump,\" not a release. **A version bump to `core/VERSION` IS a release. If you are\nabout to run `scripts/release/sync-versions.sh` for ANY reason — including re-pinning this\nrepo as a step in some OTHER repo's release — stop and follow this skill from step 1,\nincluding the candidate dispatch. Do not improvise the sequence from memory even if you\ndid it correctly last time; do not skip it because \"this bump is small\" or \"we're in a\nhurry.\"** v0.20.31 additionally caught a second, previously-undetected instance of the\nsame class of bug in the SAME commit: `RunAnywhereMLXRuntime` in the Flutter podspec (which\n`RAC_CHECKSUM_SKIP` exempts from CI verification, so it fails silently for a consumer\ninstead of failing `publish`) was ALSO stale, and had to be independently re-verified\nagainst the same candidate build — see step 3's checksum-location table; check every row\nin it, not just the one CI complained about.\n\nCompanion skill: **sdk-publish** — invoke it at the very end (step 10) for the actual\nnpm / Maven / pub.dev / SwiftPM-distribution publishing. This skill's job stops at a\npublished (non-draft) GitHub Release with correct assets.\n\n---\n\n## 0. Orientation facts (confirm these still hold before relying on them)\n\n- Canonical version file: `core/VERSION` (single line, e.g. `0.20.24`). `core/VERSIONS`\n  also carries a `PROJECT_VERSION=` line that must agree — `sync-versions.sh` updates\n  both, `auto-tag.yml` reads `core/VERSIONS`, `check_release_version_coherence.sh` reads\n  `core/VERSION`. Don't hand-edit either; always go through the script.\n- `scripts/release/sync-versions.sh <version>` — bumps ~20 files (Package.swift,\n  gradle.properties, all package.json/pubspec.yaml, Kotlin/Swift/TS version constants,\n  AGENTS.md). Accepts a leading `v` and strips it.\n- `scripts/validation/gates/check_release_version_coherence.sh` — the gate that enforces\n  \"PR changing VERSION must carry exactly one `release:patch|minor|major` label, and the\n  new version must be exactly base+1 for that bump type.\" Reads `PR_BASE_SHA` and\n  `PR_RELEASE_LABELS_JSON` env vars when run under CI; locally it only checks the\n  canonical-version-format part.\n- `.github/workflows/pr-build.yml` job **`centralization`** is what actually runs the\n  coherence gate in CI (`Release-train version coherence` step), alongside\n  `check_typescript_centralization.sh`, `check_flutter_centralization.sh`,\n  `check_gradle_centralization.sh`, `check_wasm_provenance_contract.sh`,\n  `check_swift_dist_repo_sync.sh`, `check_agents_claude_sync.sh`,\n  `check_no_hardcoded_defaults.sh`. This job's trigger types are\n  `[opened, synchronize, reopened, labeled, unlabeled]` — **adding the release label\n  after opening the PR fires a fresh run**, which is how the gate clears without a new\n  commit (see step 1).\n- `.github/workflows/release.yml` — the build/package/draft-release workflow. Triggers:\n  tag push `v*.*.*` OR `workflow_dispatch` (works on **any branch**, not just tags) with\n  inputs `version` (required), `validate_external_starters` (bool, default false),\n  `publish_from_run_id` (optional), `reuse_native_web_run_id` (optional).\n- `.github/workflows/auto-tag.yml` — fires on `pull_request: closed` to `main` when the\n  merged PR carries a `release:*` label. Verifies the version bump matches the label,\n  pushes the `v<version>` tag, and **explicitly dispatches a fresh `release.yml` run at\n  that tag** (plain `git push` of a tag with `GITHUB_TOKEN` does not auto-trigger\n  workflows, so it calls `gh workflow run release.yml --ref v<version> -f version=<version>`\n  itself). This is the redundant rebuild you must cancel — see step 7.\n- Branch protection on `main`: check\n  `gh api repos/RunanywhereAI/runanywhere-sdks/branches/main/protection` — 2 approvals +\n  code owner review required, but look for a `bypass_pull_request_allowances` (users/teams/apps)\n  list. If your `gh api user --jq .login` is on that list, `gh pr merge --squash --admin`\n  is a legitimate, pre-configured bypass — not a hack. If not on the list, stop and ask a\n  human to approve/merge.\n- Never let any `qhexrt`-named asset reach the public asset set. `release.yml`'s own\n  manifest-assertion step (`find release-flat -maxdepth 1 -type f -iname '*qhexrt*'`)\n  hard-fails the run if one is found — this is enforced, not just policy.\n- **Electron IS part of `release.yml` now** (as of `fix/electron-cicd-staging-url-npu-ane`,\n  2026-08-21) — this reverses the prior \"deliberately separate\" policy noted below by an\n  earlier pass; the repo owner explicitly asked for Electron to produce ready-to-publish\n  artifacts through the real release train like every other SDK. `native_electron`\n  invokes `.github/workflows/electron-native-package.yml` as a **reusable workflow**\n  (`workflow_call`) rather than duplicating its ~400 lines of hard-won native-build logic\n  inline; `sdk_electron` downloads the resulting `electron-packaged-tarballs` artifact,\n  runs `scripts/release/prepublish_check.py` on every tarball, computes checksums, and\n  splits public vs. QHexRT-private before anything reaches `release-flat`. `publish`\n  hard-gates on `sdk_electron` succeeding (same tier as `sdk_kotlin`/`sdk_web`/\n  `sdk_react_native`) but treats `native_electron`'s own result as advisory, because that\n  job's result folds in `native_win_arm64_qhexrt` — a single, personally-owned\n  self-hosted NPU runner whose outage should cost only `@runanywhere/electron-qhexrt`\n  for that release, not the whole SDK train (`native_win_x64_and_package`, the job that\n  actually produces the tarballs, tolerates a failed/skipped QHexRT lane and still packages\n  the other five real artifacts). Like every other SDK here, this still does **not**\n  `npm publish` anything — it only produces the tarballs as GitHub Release assets\n  (public ones) / a workflow artifact (the private QHexRT one) for a human to publish by\n  hand, matching the \"build automated, publish manual\" shape everywhere else.\n  **Electron is no longer WIP on a branch** — the old root `AGENTS.md`/`CLAUDE.md`\n  \"Active issues\" section describing `smonga/electron_upgrade` as in-progress is gone\n  entirely as of this checkout; `bindings/electron/` is a fully merged, real SDK on\n  `main` with its own `AGENTS.md`. Re-check that section is still absent before trusting\n  this note — if it has reappeared, something regressed.\n  See `thoughts/shared/plans/electron_cicd_staging_url_and_npu_ane_pipeline.md` for the\n  full history (this pipeline shipped the placeholder-URL bug three times before\n  `electron-native-package.yml` existed).\n  **QHexRT public/private boundary — CONFIRMED POLICY, not an open flag**: the repo\n  owner explicitly confirmed (2026-08-20) that `@runanywhere/electron-qhexrt` stays\n  public on npm intentionally (it already is, and is a real, working precedent worth\n  studying before touching QHexRT packaging elsewhere) — the QHexRT-stays-private\n  policy enforced everywhere else in this pipeline is specifically about the\n  **GitHub-Release-side** asset set (e.g. Android), a different distribution channel.\n  This is settled; stop re-flagging it as an unresolved contradiction. Only re-raise it\n  if you find evidence the policy has changed.\n\n---\n\n## 0b. NeuRT is a PINNED PREBUILT now — check this before anything else\n\n`native_ios` no longer compiles the private `neurun` repo. It downloads archives that\nrepo publishes, pinned by tag + SHA-256 in `core/VERSIONS`, the same shape as sherpa-onnx:\n\n```\nNEURUN_REPO=RunanywhereAI/neurun      # shared: one repo publishes BOTH engines\nNEURT_RELEASE_TAG=v<version>          # per-engine: a release may carry one lane only\nNEURT_RAC_ABI_VERSION=9\nNEURT_MACOS_ARM64_SHA256=…\nNEURT_IOS_ARM64_SHA256=…\nNEURT_IOS_ARM64_SIMULATOR_SHA256=…\n```\n\n`NEURUN_REPO` is shared because one neurun release carries both engines. The **tags stay\nper-engine** on purpose: that repo can cut a release for one lane only, so a given tag may\ncarry just one engine's assets.\n\n**Run the gate after re-pinning** — it verifies the pins against the release itself, so a\nmistyped or locally-computed value fails there instead of mid-build:\n\n```bash\nNEURUN_TOKEN=\"$(gh auth token)\" bash scripts/validation/gates/check_engine_prebuilt_pins.sh\n```\n\n### Local engine development — there is no source mode\n\nThe engine has no `NEURT_ROOT` path any more; nothing here compiles engine source. To iterate\non NeuRT, build a slice in the neurun checkout and point the engine at it — no release, no\ntag, no pin edit:\n\n```bash\n# in neurun\nNeuRT/tools/scripts/package-rac-dist.sh --sdk-root <this repo> --version 0.0.0-dev \\\n    --slice macos-arm64 --out /tmp/devdist\nmkdir -p /tmp/devslice && tar -xzf /tmp/devdist/neurt-macos-arm64-v0.0.0-dev.tar.gz \\\n    -C /tmp/devslice --strip-components=1\n\n# here\nNEURT_PREBUILT_ROOT=/tmp/devslice cmake -S . -B build/dev -G Ninja \\\n    -DRAC_BUILD_BACKENDS=ON -DRAC_BACKEND_NEURT=ON -DRAC_RUNTIME_COREML=ON\n# expect: Mode: PREBUILT (macos-arm64, ABI 9)\n```\n\n`NEURT_PREBUILT_ROOT` is a **single-slice** root, which is why\n`build-core-xcframework.sh` deliberately ignores it: packaging needs all three slices, so\nhonouring a single-slice override there would let one staged slice satisfy the guard and\nsilently package two non-routable stubs. Use an obviously-not-a-release version like\n`0.0.0-dev` so a dev slice can never be mistaken for a published one.\n\n**Why this changed, and why it matters to you specifically.** `engines/neurt/CMakeLists.txt`\nused to resolve a `NEURT_ROOT` checkout of that private repo and **regex-parse its root\n`CMakeLists.txt`** for a source list. Under that arrangement, commons widened\n`rac_llm_stream_callback_fn` with `tokens_in_delta`, the pinned neurun commit still passed\n4 arguments, and `native_ios` died with *\"too few arguments to function call, expected 5,\nhave 4\"* on **v0.20.15, v0.20.16 and v0.20.17**. Every packaging job needs `native_ios`,\nso all three shipped **iOS-only — 18 assets where a healthy release has 55+**. That is the\nsingle most expensive failure this runbook exists to prevent, and it came from this seam.\n\n**Before opening the release PR**, confirm the pin is current:\n\n```bash\ngrep -A5 '^NEURT_REPO' core/VERSIONS\ngh release view \"$(grep '^NEURT_RELEASE_TAG=' core/VERSIONS | cut -d= -f2)\" \\\n    --repo RunanywhereAI/neurun --json assets --jq '.assets[].name'\n```\n\nExpect six assets — three `neurt-<slice>-v<version>.tar.gz` plus their `.sha256` sidecars.\n\n**If the plugin ABI changed this cycle** (`RAC_PLUGIN_API_VERSION` in\n`core/include/rac/plugin/rac_plugin_entry.h`), the pinned archives are invalid and\n`native_ios` will fail at the download step. Cut a neurun release first — that repo's\n`release_sdk_archives` skill is the runbook — then re-pin both `NEURT_RELEASE_TAG` and\n`NEURT_RAC_ABI_VERSION` here. The downloader refuses to run if `NEURT_RAC_ABI_VERSION`\ndisagrees with the header, so this cannot be half-done.\n\nProve it locally before trusting a tag build:\n\n```bash\nNEURUN_TOKEN=\"$(gh auth token)\" bash scripts/build/download-neurt.sh --all\n```\n\nThat verifies each tarball's SHA-256 **and** that its `RECEIPT.json` names the same ABI\nversion this repo defines. Then check configure prints `Mode: PREBUILT (<slice>, ABI N)`.\n`SOURCE` means a local neurun checkout was picked up (prebuilt is meant to win, so the\ndownload did not land); `SHELL` means the engine is **not routable** and a release built\nnow would ship an engine that registers, claims Apple availability and serves nothing.\n\n`release.yml` treats the downloader's exit **3** (\"no token — degrade to the shell\") as\n**fatal**. That degrade is right for a fork PR and catastrophic for a release.\n\n### The complete engine matrix — who consumes what, and how\n\nThere are only **three fetch points in the entire repo**. Everything else consumes an\nsdks artifact, which is why a single fetch fix can repair five SDKs at once — and why a\nsingle missing fetch can silently break several.\n\n```\nneurun release (private)\n  ├─ native_ios         ──NeuRT x3 slices──►  RABackendNeuRT.xcframework\n  │                                              ├─ Swift  (Package.swift binary target)\n  │                                              ├─ Flutter / React Native (co-vendored\n  │                                              │   in the onnx package's podspec)\n  │                                              └─ rcli-macos (stages it, then GREPS\n  │                                                  `backends --json` for neurt)\n  ├─ native_android     ──QHexRT arm64-v8a──►  one build stages jniLibs into\n  │                                              Kotlin + React Native + Flutter\n  └─ electron           ──NeuRT macos-arm64 AND QHexRT win-arm64──►  its own natives\n```\n\n| target | NeuRT | QHexRT | note |\n|---|---|---|---|\n| Swift (iOS/macOS) | ✅ xcframework | — | NeuRT is Apple-only |\n| Flutter / React Native (Apple) | ✅ xcframework | — | co-vendored in the onnx package |\n| Flutter / React Native (Android) | — | ✅ jniLibs | from the same Android build as Kotlin |\n| Kotlin (Android) | — | ✅ jniLibs | arm64-v8a only; the only ABI with a Hexagon NPU |\n| Electron macOS | ✅ fetches | — | builds its own natives |\n| Electron win-arm64 | — | ✅ fetches | |\n| rcli macOS | ✅ xcframework | — | **best check in the tree** (below) |\n| rcli linux / windows-x64 | — | — | correct: NeuRT is Apple-only, QHexRT is win-arm64 |\n\n**`rcli-macos` is the strongest verification that exists here.** It greps\n`backends --json` for `\"name\":\"neurt\"` — and a non-routable shell *refuses\nregistration*, so that grep proves the engine is **routable**, not merely present. Copy\nthis pattern when adding an engine rather than checking for a file on disk.\n\n**Two silent-failure traps this matrix exists to prevent:**\n\n- **`RAC_BACKEND_<X>=ON` does not mean the engine works.** Without a payload the engine\n  builds as a non-routable shell and registration is refused — no build error. Android\n  shipped that way for months, and `rcli-macos-release` still sets\n  `RAC_BACKEND_NEURT: ON` regardless of whether a payload was fetched.\n- **`copy_if_exists` in `build-core-android.sh` silently no-ops** when a lib was never\n  built. That is the same guarded-copy pattern that shipped Windows with zero engines for\n  several releases. The release now asserts `librunanywhere_qhexrt.so` is really inside\n  the arm64-v8a archive.\n\n`scripts/validation/gates/check_engine_prebuilt_pins.sh` asserts all of the above plus\nthe pins, the ABI, header drift against the receipt, and (with a token) the release\ncontents. Run it before cutting a release; it is also wired into `pr-build` offline.\n\n### QHexRT is pinned the same way — check it too\n\nThe Hexagon-NPU engine crosses the identical boundary, published by the same neurun release:\n\n```\nQHEXRT_RELEASE_TAG=v<version>\nQHEXRT_ARM64_V8A_SHA256=…      QHEXRT_ARM64_V8A_RECEIPT=…\nQHEXRT_WIN_ARM64_SHA256=…      QHEXRT_WIN_ARM64_RECEIPT=…\n```\n\nTwo pins per ABI, not one: the tarball checksum **and** the payload's own build-receipt\nhash. The receipt is the directory name under `engines/qhexrt/prebuilt/versions/`, so\npinning it is what makes the pin describe the bytes rather than trusting the archive's\nown claim about itself.\n\n```bash\nNEURUN_TOKEN=\"$(gh auth token)\" bash scripts/build/download-qhexrt.sh --abi arm64-v8a\nNEURUN_TOKEN=\"$(gh auth token)\" bash scripts/build/download-qhexrt.sh --abi win-arm64\n```\n\n**`current` selects ONE ABI at a time.** Both payloads can sit in `versions/` together, but\na build targets one platform — so re-run the downloader when switching between an Android\nand a Windows build. The downloader ends by running `validate-qhexrt-prebuilt.py` itself,\nso a bad payload fails at the pin rather than mid-build.\n\n**The QHexRT lane in neurun is deliberately non-blocking.** It runs on a single\nself-hosted Snapdragon box (QAIRT is licence-gated and exists on no hosted runner). If\nthat machine is offline, neurun's release publishes the Apple artifacts and **warns** that\nno QHexRT payload was produced — and the SDK simply keeps its previously pinned prebuilt.\nThat is correct behaviour, not a failure: do not block an SDK release on it. Only treat it\nas blocking if this release is specifically meant to ship new NPU kernels.\n\n**Do not hand-stage `engines/qhexrt/prebuilt/`.** That was the old flow, and it is how the\nElectron win-arm64 lane once pinned a receipt staged months earlier and silently shipped\nNPU kernels from an old commit. If the tree looks stale, re-pin and re-download; never\ncopy a payload in by hand.\n\n---\n\n## 1. Decide the version and open the release PR\n\nCheck for an already-open release PR first — don't duplicate work:\n\n```bash\ngh pr list --repo RunanywhereAI/runanywhere-sdks --label release:patch --label release:minor\n```\n\n(gh ORs multiple `--label` flags into a single AND-filtered query per invocation is not\nguaranteed across all gh versions — if unsure, run the two label queries separately:\n`--label release:patch` then `--label release:minor`, plus `--label release:major`.)\n\nPick a patch/minor/major bump based on the changes since the last tag. Then:\n\n```bash\ngit checkout -b release/v<version>\n./scripts/release/sync-versions.sh <version>      # e.g. 0.20.25 (no 'v' needed, either works)\nbash scripts/validation/gates/check_release_version_coherence.sh\n```\n\nThe coherence script may point out generated locks/changelogs that need regenerating\n(e.g. package-lock.json version bumps it expects but didn't find, or a missing CHANGELOG\nheading for the new version). Fix everything it flags, and add **explicit, human-written\nchangelog entries** — the gate requires a heading for the new version to exist; it does\nnot write prose for you.\n\nOpen the PR and **apply exactly one `release:patch` or `release:minor` (or `release:major`)\nlabel** — this is required, not optional. Two failure modes to know:\n\n- If you forget the label and push a commit later, the `centralization` job's\n  `Release-train version coherence` step fails.\n- **Adding the label after the PR is already open does NOT get picked up by the run that\n  already executed.** Because `pr-build.yml`'s `centralization` job triggers on\n  `labeled`/`unlabeled` PR events, applying the label fires a **new** run of the whole\n  job — that new run is what goes green. Don't push an empty commit to \"retrigger\" it;\n  just add/re-add the label (or use `gh pr edit <pr> --add-label release:patch`, and if it\n  was already present, remove + re-add to force the event).\n\n---\n\n## 2. Get REAL native-artifact checksums BEFORE merging — dispatch a release candidate\n\nDo not merge on faith that native builds will succeed. `release.yml`'s\n`workflow_dispatch` trigger works on **any branch**, including your unmerged release\nbranch — use it to build a full release candidate first:\n\n```bash\ngh workflow run release.yml --ref release/v<version> -f version=<version>\n```\n\nGet the run id and watch it:\n\n```bash\ngh run list --repo RunanywhereAI/runanywhere-sdks --workflow=release.yml --limit 5\n```\n\n**Do not block synchronously waiting on it.** Load the `Monitor` tool (or a background\npoll loop) to watch for completion/failure, and keep doing other release prep (changelog\nreview, CodeRabbit thread triage, branch-protection check) in the meantime. Keep\nnotifications to real state changes only — don't ping on every intermediate job\ncompleting, only on the run's overall failure or full completion.\n\n### REAL job durations — measured, not guessed. NEVER cancel a job for being \"slow\".\n\nMeasured on the 0.20.25 train (2026-08-21/22). A release train legitimately takes\n**4-5 hours** wall-clock when `native_web` has to build.\n\n| Job | Real duration | Notes |\n|---|---|---|\n| **`native_web`** (release.yml) | **~3h51m** | The long pole, by far. Its `Vendor ONNX Runtime and Sherpa-ONNX WASM archives` step alone is **~71 minutes** — it BUILDS onnxruntime + sherpa to WASM from source; it is not a git clone. Then 4 more WASM builds: CPU core+llamacpp+onnx ~89m, WebGPU llamacpp ~37m, WebGPU onnx+sherpa ~33m. |\n| `native_ios` | ~60-75 min | XCFrameworks + MLX. No longer compiles neurun — it downloads the pinned NeuRT archives (see §0b), which is seconds, not minutes. |\n| `swift-spm` (pr-build) | **69-85 min** | `Build XCFramework` step is most of it. Measured across 6 consecutive `main` runs (2026-08-21/22): 69, 78, 75, 85, 77, 69. A run sitting at ~78 minutes is **healthy**, not hung — check this row before reaching for cancel. |\n| `kotlin-android` (pr-build) | ~40 min | |\n| `python-windows` (pr-build) | ~40 min | |\n| `electron (windows-2022)` (electron-sdk-ci) | ~40 min | |\n| `rcli-windows` | ~30 min | |\n| `native_electron / native_win_arm64_qhexrt` | ~28 min | Self-hosted; may sit **queued** for a long time first if the single runner is busy. Queued ≠ hung. |\n| `native_electron / native_win_x64_and_package` | ~35-45 min | |\n| `wasm` (pr-build) | ~12 min | **Do NOT use this as a proxy for `native_web`.** Totally different scope. |\n\n**The mistake this table exists to prevent**: `pr-build.yml`'s `wasm` job takes ~12\nminutes, so a ~50-minute `native_web` \"vendor\" step looks hung. It is not. On the 0.20.25\ntrain this misread caused two healthy `native_web` jobs to be cancelled at 2h19m and\n~55m, wasting ~3 hours and two full candidate builds. Before concluding \"hung\", check\nstep-level progress and compare against THIS table:\n\n```bash\ngh run view <run-id> --repo RunanywhereAI/runanywhere-sdks --json jobs \\\n    --jq '.jobs[] | select(.name==\"native_web\") | {startedAt, steps: [.steps[] | select(.status==\"in_progress\") | .name]}'\n```\n\n### `reuse_native_web_run_id` is the correct way to avoid re-paying that ~4 hours\n\nIf a run needs redoing and **nothing under `bindings/web/wasm/`, `engines/`, or `core/`\nchanged** since a run whose `native_web` succeeded, reuse it instead of rebuilding:\n\n```bash\ngit diff --stat <good-run-sha> <current-sha>      # prove the scope first\ngh workflow run release.yml --ref release/<version> \\\n    -f version=<version> -f reuse_native_web_run_id=<good-run-id>\n```\n\n`native_web` then shows `skipped`, and `sdk_web` + `validate_consumer_web` still run and\npass against the reused artifact — which is also your proof the reuse was valid. Track the\nrun id that **originally built** it: that is the id you keep citing (see the chaining note\nin step 7).\n\n---\n\n## 3. Sync checksums once native_ios finishes\n\nPre-tag, `gh release download` will not work (there is no release yet) — pull the run's\nartifact directly:\n\n```bash\ngh run download <candidate-run-id> --repo RunanywhereAI/runanywhere-sdks \\\n    --name native-ios-macos --dir release-artifacts/native-ios-macos\n```\n\nVerify every `.zip` against its `.sha256` sidecar **yourself** before trusting it:\n\n```bash\ncd release-artifacts/native-ios-macos\nfor f in *.zip; do shasum -a 256 -c \"${f}.sha256\" || echo \"MISMATCH: $f\"; done\ncd -\n```\n\nThen sync:\n\n```bash\nbindings/swift/scripts/sync-checksums.sh release-artifacts/native-ios-macos\n```\n\n**KNOWN FACT — do not treat this as a bug**: `RunAnywhereMLXRuntime` (and occasionally\n`RACommons`) are **not byte-reproducible** between rebuilds of identical source. Expect\ntheir checksums to differ from any prior candidate build every single time you rebuild,\neven with zero source changes. Only investigate if a checksum for a framework that\nhistorically IS reproducible (e.g. plain XCFrameworks with no Swift codegen step)\nsuddenly changes.\n\n**Where each checksum actually lives — these are NOT all in one file:**\n\n| Manifest | Carries |\n|---|---|\n| root `Package.swift` | `RACommons` + the 5 backend XCFrameworks (6 checksums total) |\n| nested Flutter `Package.swift` (under `bindings/flutter/`) | same 6 |\n| `bindings/flutter/packages/runanywhere_mlx/ios/runanywhere_mlx.podspec` → `mlx_checksums` hash | `RunAnywhereMLXRuntime`, `RunAnywhereMLXMetal`, `RunAnywhereMLXResources` — **only here**, nowhere else |\n\nAfter `sync-checksums.sh` runs, verify centralization didn't drift and re-run the version\ncoherence gate locally:\n\n```bash\nbash scripts/validation/gates/check_flutter_centralization.sh\nbash scripts/validation/gates/check_release_version_coherence.sh\n```\n\nCommit and push the checksum-sync commit to the release branch:\n\n```bash\ngit add Package.swift bindings/flutter/Package.swift \\\n    bindings/flutter/packages/runanywhere_mlx/ios/runanywhere_mlx.podspec\ngit commit -m \"release: sync v<version> checksums from candidate run <candidate-run-id>\"\ngit push\n```\n\n### THIS STEP IS NOT OPTIONAL — skipping it fails `publish` after the tag already exists\n\n**What happens if you skip it** (observed on the 0.20.25 train): every build and package\njob goes green, `publish` gets all the way past flatten and the asset-manifest assertion,\nthen dies at **`Verify built Apple checksums match tagged package contracts`**:\n\n```\nmismatch: RACommonsBinary\n  Package.swift: 0609dae6...      release zip: 1ae55ae4...\nmismatch: RABackendLlamaCPPBinary\n  Package.swift: 0597442f...      release zip: 7062f7c8...\n>> Done. 8 processed, 0 missing, 4 failed.\nERROR: built Swift archives do not match the immutable tagged manifest\n```\n\nNote `8 processed / 4 failed` is really **2 distinct archives counted twice** (root\nmanifest + Flutter manifest each). On that train `RACommons` and `RABackendLLAMACPP`\ndrifted while ONNX/Sherpa/NeuRT/MLX/MLXMetal/MLXResources all matched — so \"6 of 8 passed\"\nis a normal-looking partial failure, not evidence of something exotic.\n\n**Recovery once the tag already exists** (this is safe and was done on 0.20.25):\n\n1. Confirm nothing was actually published — a failed `publish` dies *before* creating the\n   release, so there is usually nothing to undo:\n   ```bash\n   gh release view v<version> --repo RunanywhereAI/runanywhere-sdks   # expect \"release not found\"\n   ```\n2. Download the **exact artifacts the release will publish** (the candidate run named in\n   `publish_from_run_id`), verify each against its sidecar, and sync on a branch off `main`.\n3. Merge that branch, then **re-point the tag** at the new commit. `git push origin --delete`\n   can hang on flaky networks; the API is more reliable:\n   ```bash\n   gh api -X DELETE repos/RunanywhereAI/runanywhere-sdks/git/refs/tags/v<version>\n   gh api -X POST   repos/RunanywhereAI/runanywhere-sdks/git/refs \\\n       -f ref=\"refs/tags/v<version>\" -f sha=\"<new-main-sha>\"\n   ```\n   Only do this while **no release exists and nothing has been published**. If a release\n   was already published, do NOT move the tag — cut the next patch version instead.\n4. **Creating a tag via the API DOES trigger `release.yml`'s `push` trigger** (unlike a\n   `GITHUB_TOKEN` git-push of a tag, which does not). So immediately after re-tagging you\n   will have TWO runs: a full rebuild from the `push` event, and your own\n   `workflow_dispatch`. Identify by event and cancel the `push` one:\n   ```bash\n   gh run list --repo RunanywhereAI/runanywhere-sdks --workflow=release.yml --limit 3 \\\n       --json databaseId,event,headBranch --jq '.[] | \"\\(.databaseId) \\(.event) \\(.headBranch)\"'\n   gh api -X POST repos/RunanywhereAI/runanywhere-sdks/actions/runs/<push-run-id>/cancel\n   ```\n\nCheapest prevention: run `sync-checksums.sh --check <zip_dir>` against the candidate's\nartifacts and require **`N processed, 0 missing, 0 failed`** before you merge the release\nPR at all.\n\n**Recurred on v0.20.30 and again on v0.20.31** — same `mismatch: RACommonsBinary` /\n`mismatch: RACommons` failure, same root cause (step 2's candidate dispatch skipped\nentirely both times), fixed the same way both times: compute the real checksum from the\nalready-built release-artifact zip (`gh run download <failed-run-id> --name\nnative-ios-macos`), commit it on a fix branch, merge, then move the tag (§10d) to\nre-trigger `release.yml`. On v0.20.31 this ALSO surfaced the `RunAnywhereMLXRuntime`\npodspec entry as independently stale — `RAC_CHECKSUM_SKIP` means `publish` never checks\nit, so it would have shipped broken to Flutter consumers silently. **When fixing this\nfailure after the fact, check every row of the checksum-location table in step 3, not\nonly the archive named in the CI error** — the ones `RAC_CHECKSUM_SKIP` exempts are\nexactly the ones that won't tell you.\n\n---\n\n## 4. Watch for the \"package intentionally doesn't bundle a sibling dep\" bug class\n\nThis exact release hit a real bug in `bindings/web/scripts/package-sdk.sh`:\n`@runanywhere/web-onnx` deliberately does **not** bundle its own `proto-ts` copy (only\n`core`/`llamacpp` do — enforced by `bindings/web/scripts/validate_public_packages.py` and\nmirrored in `bindings/react-native/scripts/validate_public_packages.py`), but the\npackaging script's install-smoke-test (`verify_candidate_install`, around line 165 of\n`package-sdk.sh`) never supplied a resolution target for that unbundled dependency, so\n`npm install` reached out to the real npm registry for a not-yet-published version and\nfailed with `ETARGET`.\n\nFix pattern (already applied, keep it this way): the `onnx` call to\n`verify_candidate_install` passes the just-built local `proto-ts` tarball\n(`$PROTO_ARCHIVE`, built via `npm pack ../proto-ts`) as an explicit extra install arg —\nsee lines ~209-233 of `bindings/web/scripts/package-sdk.sh`, where `core` and `llamacpp`\ncalls omit it (they bundle proto-ts themselves) but `onnx`'s call appends `\"$PROTO_ARCHIVE\"`.\n\n**Generalize this as a standing check for every release**: grep for the asymmetric\nbundling flags before trusting any packaging script —\n\n```bash\ngrep -n \"expects_bundled_proto\\|--bundle\" \\\n    bindings/web/scripts/package-sdk.sh bindings/react-native/scripts/package-sdk.sh\n```\n\nWhenever a package intentionally does NOT bundle a sibling first-party dependency, its\nlocal pre-publish verification step must supply that dependency some other way (a local\ntarball) — the real registry version genuinely does not exist yet at candidate-build\ntime, and letting `npm`/`yarn` hit the real registry will `ETARGET`.\n\n**Before trusting any packaging script is bug-free, actually run it** against the real\ndownloaded native artifacts and check the exit code — do not just read the code and\nassume it's fine:\n\n```bash\ncd bindings/web && ./scripts/package-sdk.sh   # then check $?\ncd bindings/react-native && ./scripts/package-sdk.sh\n```\n\n### 4b. The two Electron-specific release bugs (both fixed — know the shapes)\n\nThe 0.20.25 train was the **first** release to run Electron packaging inside `release.yml`,\nand it surfaced two defects that no local test or green PR check could have caught. Both\nare fixed; recognize the shapes because they generalize.\n\n**(1) `sync-versions.sh` has a hardcoded package list, and new packages get missed.**\n`bindings/electron/packages/neurt/package.json` sat at the previous version while every\nsibling moved, because `neurt` was added (electron#734) after that list was last touched.\nSymptom: `native_win_x64_and_package` fails with\n\n```\nERROR: npm pack did not produce .../runanywhere-electron-neurt-<version>.tgz\n```\n\n— because `npm pack` correctly produced `...-neurt-<OLD-version>.tgz` from the stale\nmanifest. **Whenever you add a package anywhere, add it to `sync-versions.sh` the same\nday.** Cheap pre-flight check that catches the whole class:\n\n```bash\ngrep -h '\"version\"' bindings/electron/package.json bindings/electron/packages/*/package.json \\\n    bindings/electron/native/package.json | sort -u     # expect exactly ONE distinct version\n```\n\n**(2) `publish` downloads EVERY artifact, including intermediate hand-off bundles.**\n`publish`'s `Download all artifacts` step has no name filter. `electron-packaged-tarballs`\nis `native_win_x64_and_package`'s raw output whose only purpose is feeding `sdk_electron`,\nwhich re-uploads the curated public set as `sdk-electron`. Flattening both duplicated all\n5 Electron tarballs *and* dragged in the raw bundle's own `proto-ts` copy, which collided\nwith `sdk_web`'s:\n\n```\n::error::flatten collision: release-flat/runanywhere-proto-ts-<v>.tgz already exists\n::error::Refusing to publish — 6 filename collision(s) detected during flatten\n```\n\nFixed by pruning that directory in the flatten `find`\n(`-type d -name \"electron-packaged-tarballs\" -prune -o -type f \\( ... \\)`). **Standing\nrule: any new intermediate artifact must be either excluded from the flatten or named so\nit cannot collide.** When editing that `find`, test the exact expression against a fake\n`release-artifacts/` tree locally first — `-prune -o` + `-print0` is easy to get subtly\nwrong and the only other way to find out costs a full release run.\n\n---\n\n## 5. Resolve CodeRabbit (or any bot) review findings properly\n\nDo not resolve a review thread without a real, evidence-backed reply. Steps:\n\n1. Fetch unresolved threads via GraphQL:\n\n```bash\ngh api graphql -f query='\nquery($owner:String!, $repo:String!, $pr:Int!) {\n  repository(owner:$owner, name:$repo) {\n    pullRequest(number:$pr) {\n      reviewThreads(first:100) {\n        nodes { id isResolved comments(first:10) { nodes { id databaseId body } } }\n      }\n    }\n  }\n}' -f owner=RunanywhereAI -f repo=runanywhere-sdks -F pr=<pr-number>\n```\n\n2. For each unresolved thread, reply via REST with `in_reply_to` set to the comment's\n   `databaseId` (not the GraphQL node id), citing concrete evidence — a real run id,\n   a real checksum, a real commit SHA, not \"looks fine\":\n\n```bash\ngh api -X POST repos/RunanywhereAI/runanywhere-sdks/pulls/<pr-number>/comments \\\n    -f body=\"Verified against candidate run <run-id>: RunAnywhereMLXRuntime checksum\nmatches release-artifacts/native-ios-macos/RunAnywhereMLXRuntime-ios-v<version>.zip.sha256.\nSee commit <sha>.\" \\\n    -F in_reply_to=<comment-database-id>\n```\n\n3. Only then resolve, via GraphQL mutation on the thread id from step 1:\n\n```bash\ngh api graphql -f query='\nmutation($threadId:ID!) { resolveReviewThread(input:{threadId:$threadId}) { thread { isResolved } } }\n' -f threadId=<thread-node-id>\n```\n\n---\n\n## 6. Merge\n\nBefore merging, verify:\n\n```bash\ngh pr view <pr-number> --repo RunanywhereAI/runanywhere-sdks --json mergeable,reviewDecision\n```\n\nZero unresolved review threads (re-check the GraphQL query from step 5 — it should return\nnone with `isResolved: false`), and `mergeable == \"MERGEABLE\"`.\n\nConfirm bypass eligibility before relying on `--admin`:\n\n```bash\ngh api repos/RunanywhereAI/runanywhere-sdks/branches/main/protection \\\n    --jq '.required_pull_request_reviews.bypass_pull_request_allowances'\ngh api user --jq .login\n```\n\nIf your login appears in that allowance list (users, or a team/app you belong to):\n\n```bash\ngh pr merge <pr-number> --repo RunanywhereAI/runanywhere-sdks --squash --admin\n```\n\nIf not on the list, **stop** — ask a human to approve or merge instead of forcing it.\n`--admin` is a legitimate configured bypass only for allow-listed identities; do not use\nit to route around review for anyone else.\n\n---\n\n## 7. GOTCHA — cancel the auto-tag-dispatched rebuild, dispatch a publish-only run instead\n\nThe moment the PR merges, `auto-tag.yml` runs, pushes the `v<version>` tag, and **itself**\ndispatches a brand-new full `release.yml` run at that tag (see orientation section — plain\ntag pushes via `GITHUB_TOKEN` don't auto-trigger, so the workflow does it explicitly).\nThis is redundant — you already have a fully green, checksum-verified candidate from step\n2-3 — and wasteful, because Apple/MLX bytes will drift again for zero benefit (see the\nnon-reproducibility fact in step 3).\n\n**Cancel it immediately, before it burns real compute** (ideally within the first minute\nor two, before native_ios starts real work):\n\n```bash\ngh run list --repo RunanywhereAI/runanywhere-sdks --workflow=release.yml --limit 5\ngh api -X POST repos/RunanywhereAI/runanywhere-sdks/actions/runs/<auto-triggered-run-id>/cancel\n```\n\nThen dispatch a **publish-only** run that reuses your already-verified candidate's exact\nbytes instead of rebuilding:\n\n```bash\ngh workflow run release.yml --ref v<version> \\\n    -f version=<version> \\\n    -f publish_from_run_id=<candidate-run-id>\n```\n\n**CRITICAL SUB-GOTCHA**: if your candidate run in step 2 itself passed\n`reuse_native_web_run_id=<X>` to skip rebuilding WASM (a packaging-only re-release), that\ncandidate run **never uploaded its own copy of the `native-web` artifact** — it was\nskipped. `publish_from_run_id` alone will then fail during the release-manifest assertion\nwith `\"Missing or empty Web WASM payload\"`, because the publish job's artifact-download\nstep only pulls `reuse_native_web_run_id`'s artifact when that input is passed to *this*\ndispatch too (see `.github/workflows/release.yml` around the \"Download reused Web natives\"\nstep — it is gated on `inputs.reuse_native_web_run_id != ''`, independent of\n`publish_from_run_id`). You must **also** pass the same original run id:\n\n```bash\ngh workflow run release.yml --ref v<version> \\\n    -f version=<version> \\\n    -f publish_from_run_id=<candidate-run-id> \\\n    -f reuse_native_web_run_id=<X>\n```\n\nThis chains forward across releases: whichever run **originally** built `native_web` is\nthe run you keep citing at every subsequent `reuse_native_web_run_id`, not the most recent\nrun that merely borrowed it from an earlier one. Track that original run id explicitly if\nyou're doing back-to-back packaging-only releases.\n\n---\n\n## 8. Verify the draft release's asset set\n\nThe publish job only creates a **draft** GitHub Release (`softprops/action-gh-release`,\n`draft: true`) with built assets attached — it does not touch npm/Maven/pub.dev at all\n(grep `release.yml` for `npm publish`, `mavenCentral`, `pub publish` — none exist there).\n\nExpected asset count for a full, non-Windows-focused release: **~65 assets** —\n\n- 9 Apple archives (iOS/macOS XCFrameworks: RACommons + 5 backends + MLX runtime/Metal/\n  resources + CoreML/NeuRT)\n- 3 Android RACommons zips (arm64-v8a, armeabi-v7a, x86_64)\n- 1 Linux RACommons tar.gz\n- 1 Windows RACommons zip (preserved as an asset, advisory — see step 9)\n- 1 Web WASM tar.gz (`RACommons-web-v<version>.tar.gz`)\n- 3 rcli tarballs (macOS arm64, Linux x86_64, Windows x86_64 — macOS may also have a\n  notarized `.dmg` if Developer ID/notary secrets are configured; this is soft-skipped\n  otherwise, not a failure)\n- 1 Kotlin Maven repo zip\n- 8 npm tarballs (proto-ts, web core, web-llamacpp, web-onnx, RN core, RN llamacpp, RN mlx,\n  RN onnx)\n- 5 Electron npm tarballs (`runanywhere-electron-<v>.tgz` core, `-llamacpp`, `-onnx`,\n  `-sherpa`, `-neurt`) — added by `sdk_electron`; each has its own `.sha256` sidecar\n  counted separately below. **`electron-qhexrt` is deliberately excluded** — it still\n  gets built and packaged, but only as the `sdk-electron-qhexrt-private` workflow\n  artifact (not a release asset); download it separately for a manual `npm publish`.\n- 1 `MANIFEST.txt`\n- **explicitly ZERO** `qhexrt`-named files (the manifest assertion step hard-fails the run\n  if it finds any — confirm the run actually reached the publish job and didn't die there)\n\nEvery asset above except `MANIFEST.txt` has a `.sha256` sidecar, which is itself a\nseparate asset. **v0.20.25 landed at exactly 65 assets** — use that as the concrete\nbaseline rather than re-deriving the arithmetic.\n\nCross-check every checksum's `.sha256` sidecar against what got committed to\n`Package.swift` / the flutter `Package.swift` / the MLX podspec in step 3 — they must\nmatch byte for byte, because this run's artifacts are exactly what step 10 (sdk-publish)\nships to real consumers.\n\n```bash\ngh run view <publish-run-id> --repo RunanywhereAI/runanywhere-sdks --json jobs \\\n    --jq '.jobs[] | {name, conclusion}'\n\n# Asset count + the Electron set + the qhexrt-leak assertion, in three commands:\ngh release view v<version> --repo RunanywhereAI/runanywhere-sdks --json assets --jq '.assets|length'\ngh release view v<version> --repo RunanywhereAI/runanywhere-sdks --json assets \\\n    --jq '[.assets[].name | select(startswith(\"runanywhere-electron\"))] | sort'\ngh release view v<version> --repo RunanywhereAI/runanywhere-sdks --json assets \\\n    --jq '[.assets[].name | select(test(\"qhexrt\";\"i\"))]'          # MUST be []\n```\n\n### Prove the artifacts are real — don't trust green checkmarks\n\nA green `publish` only proves the assertions ran. To prove the bytes are right, run the\nrepo's own fail-closed gate against the **published** tarballs. This is the single highest\n-value verification in the whole runbook and it takes about a minute:\n\n```bash\ngh release download v<version> --repo RunanywhereAI/runanywhere-sdks \\\n    -p 'runanywhere-electron*.tgz' -D /tmp/verify\npython3 scripts/validation/gates/check_plugin_natives.py --expect-version <version> \\\n    $(for t in /tmp/verify/*.tgz; do printf -- '--tarball %s ' \"$t\"; done)\n```\n\nExpect `All N check(s) passed: plugin natives are routable and no staging-URL placeholder\nwas found.` On v0.20.25 that was **20 checks** across 5 tarballs, asserting three separate\nthings per binary: the real `STAGING_BASE_URL` is baked in (no `YOUR_STAGING_BASE_URL`\nplaceholder), `rac_commons` embeds the right version string, and each backend is actually\n**routable** (real op tables — `g_llamacpp_ops`, `g_sherpa_stt_ops`, `g_neurt_llm_ops`,\netc.), which is what distinguishes a real plugin from a same-sized shell.\n\nSanity-check the shipped layout too — a tarball missing a platform is a silent regression:\n\n| Package | Expected `prebuilds/` |\n|---|---|\n| `@runanywhere/electron` | `darwin-arm64` + `win32-x64` + `win32-arm64` |\n| `-llamacpp`, `-onnx`, `-sherpa` | `darwin-arm64` + `win32-x64` |\n| `-neurt` | `darwin-arm64` only (Apple Neural Engine) |\n| `-qhexrt` (private artifact) | `win32-arm64` only (Hexagon NPU) |\n\n---\n\n## 9. Windows handling — build/preserve only, never auto-publish\n\n`native_windows` and `native_rcli_windows` run and their outputs are **preserved** as\nrelease assets (proving the binaries build), but nothing Windows-specific is published to\nany package registry as part of the standard flow unless a human explicitly asks for it —\nthat's a deliberate, separate decision, not an oversight.\n\nFor anything that needs actual execution on Windows (smoke-testing the native Windows\nbuild, or Electron's Windows packaging), do not assume a macOS cross-build proves\nWindows correctness — SSH to the real Windows box. The team keeps a **Windows ARM64\nmachine reachable over SSH**; its hostname, user, key, and mesh-VPN node name are\noperator-specific and deliberately not recorded in this repo — read the Windows/ARM64\nhost entry out of the operator's `~/.ssh/config` and use that alias. If there is no such\nentry, the box is not configured for you: say so and ask, rather than guessing.\n\n```bash\nWIN_BOX=<the ssh alias from ~/.ssh/config>\n# Ask the mesh-VPN client whether the node is up FIRST (e.g. `tailscale status`,\n# matching on that host entry's HostName) — never just try SSH and retry on timeout.\nssh -o ConnectTimeout=8 \"$WIN_BOX\" \"whoami\"\n```\n\nIf the VPN reports the node offline/asleep, **that's not a config problem** — ask the\nuser to wake the box; don't debug SSH config against a machine that's simply off.\n\n---\n\n## 10. Publish the draft, then hand off\n\nOnce the draft's asset set is verified complete and correct (step 8):\n\n```bash\ngh release edit v<version> --repo RunanywhereAI/runanywhere-sdks --draft=false --latest\n```\n\nThis is the point where SwiftPM's `from: \"<version>\"` resolution and the download URLs\nbaked into `Package.swift` start actually working for real external consumers — draft\nrelease assets are not publicly downloadable, so nothing before this line is\nexternally live.\n\n**Hand off to the `sdk-publish` skill** for the actual npm / Maven Central / pub.dev /\nSwiftPM-distribution-repo publishing steps. This skill's job ends here.\n\n---\n\n## 10b. MANDATORY, and it turns `main` RED until you do it — cut runanywhere-swift\n\nThe moment `v<version>` exists in this repo, `check_swift_dist_repo_sync.sh` (run by\n`pr-build.yml`'s `centralization` job) **fails on every PR and every push to `main`**:\n\n```\n[FAIL] runanywhere-swift has no 0.20.25 tag (latest: 0.20.24)\n       v0.20.25 is published here, so 'from: \"0.20.25\"' is broken for every\n       SwiftPM consumer of https://github.com/RunanywhereAI/runanywhere-swift.git\n```\n\nThis is **by design**, not a bug, and it is the #1 reason a release looks \"not green\"\nafter everything else succeeded. Do not go hunting for a code defect — cut the repo:\n\n```bash\ngit clone https://github.com/RunanywhereAI/runanywhere-swift.git /tmp/ra-swift\nbindings/swift/scripts/sync-dist-repo.sh --zips <zip_dir> --tag /tmp/ra-swift\n# verify BEFORE pushing (content check passes locally; the tag check needs the push)\nRUNANYWHERE_SWIFT_DIST_REPO=/tmp/ra-swift \\\n    bash scripts/validation/gates/check_swift_dist_repo_sync.sh\ngit -C /tmp/ra-swift push origin main --follow-tags\nbash scripts/validation/gates/check_swift_dist_repo_sync.sh    # now [OK]\n```\n\nGotchas found doing this on 0.20.25:\n\n- `--check` **cannot be combined** with `--zips`/`--commit`/`--tag` (hard error). Run a\n  bare `--check <repo>` first to preview, then the real invocation separately.\n- `--check` legitimately reports `ERROR: distribution repo is out of sync` before you\n  sync — that's the preview telling you what it will change (expect the `sdkVersion` bump,\n  ~3 changed files, `0 added / 0 removed`). Non-zero exit there is not a failure.\n- The script **never pushes**; `--tag` only creates the tag locally. The dist repo tag has\n  **no `v` prefix** (`0.20.25`, not `v0.20.25`) while the monorepo tag does.\n- Running the gate locally right after `--tag` still shows `[FAIL] ... has no <version>\n  tag` while simultaneously showing `[OK] ... checkout matches: all 6 checksums`. That is\n  expected: the content check reads your local checkout, the tag check queries the real\n  remote. It clears the moment you push.\n- It chains into `sync-checksums.sh`, so run it **after** step 3's checksums are committed,\n  or the dist manifest gets stale hashes.\n\n---\n\n## 10c. Windows + self-hosted-runner traps (read before debugging a red Windows job)\n\nThese all cost a full CI cycle. The unifying fact:\n\n> The self-hosted runner service executes as `NT AUTHORITY\\NETWORK SERVICE`, which has its\n> own PATH **and** its own NTFS permissions. Anything you verify over SSH on that box tells\n> you nothing about what the job can do. `native_win_arm64_qhexrt` already carries a long\n> comment about this — read it before inventing a fix, and keep it in sync with neurun's\n> equivalent lane, which runs on the *same machine*.\n\n**Environment**\n\n- **`bash: command not found`** — Git for Windows puts `Git\\cmd` (git.exe) on PATH but not\n  `Git\\bin` (bash.exe). `actions/checkout` still works because it calls git.exe directly,\n  which makes this look unrelated to runner setup.\n- **`pwsh` does not exist on that box** — Windows PowerShell 5.1 only. Use\n  `shell: powershell`.\n- **`gh` is not installed on that box, and must not be assumed anywhere.** A QHexRT\n  download died on `gh: command not found`. The fix is deliberately *not* to install it:\n  every fetch of a private release asset now goes through\n  `fetch_release_asset()` in `scripts/build/_release_asset.sh`, which uses `curl` + the\n  REST API. `curl` ships with Windows, macOS and every Linux image we use, so this is one\n  code path everywhere instead of a per-runner provisioning step. If you add a new\n  consumer of a private asset, source that helper — do not reach for `gh`.\n  Note the helper matches the asset name **exactly**: a prefix match picks the `.sha256`\n  sidecar (its name starts with the tarball's), and you get a 65-byte \"archive\" that fails\n  much later with a confusing error.\n- **Tool availability is the single most common failure class on this runner** — `bash`,\n  `python3`, `ninja`, `gh`, GNU `tar` and NDK `.exe` suffixes have each broken a lane\n  once. When a self-hosted job fails, check \"does this tool exist as spelled\" before\n  reading any logic.\n- **`ln -s` under Git-Bash COPIES the directory** unless `MSYS=winsymlinks:nativestrict`.\n  It does not fail, so the result looks like real content and whatever validates a\n  \"must be a symlink\" invariant rejects a payload that was actually fine. Never use shell\n  `ln -s` on this box — go through `scripts/build/_selection.py`.\n- **Windows selects with a JUNCTION, not a symlink, and that is deliberate.** The runner\n  service account cannot create symlinks (below), but a junction is the same kind of\n  reparse point to a directory and needs no privilege. Two traps when reading that code:\n  `Path.is_symlink()` returns **False** for a junction (`stat.S_ISLNK` is true only for\n  `IO_REPARSE_TAG_SYMLINK`) — use `_selection.is_selection()`; and a junction stores an\n  **absolute** t","frontmatter":{"name":"sdk-release","description":"Cut a new RunAnywhere SDK release end-to-end in RunanywhereAI/runanywhere-sdks — version bump through a fully green, tagged, drafted-then-published GitHub Release. Stops BEFORE publishing to package registries (npm/Maven/pub.dev/SwiftPM dist) — that is the separate \"sdk-publish\" skill, invoked as the last step here."},"isInternal":false,"tokens":13119,"sizeBytes":50350},{"name":"SKILL.md","path":".claude/skills/sdk-test-starters/SKILL.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/.claude/skills/sdk-test-starters/SKILL.md","title":"sdk-test-starters","category":"anthropic-skill","format":"markdown","content":"---\nname: sdk-test-starters\ndescription: Run a REAL end-to-end inference smoke test (model download + load + generate, with visible output) on every RunAnywhere starter app — iOS, macOS/Swift, Android, Web, Electron — after a runanywhere-sdks release. Build success alone is never sufficient evidence.\n---\n# SDK Test Starters\n\nPost-release validation runbook for the five RunAnywhere SDK consumer apps. This\nskill exists because \"it built\" has been reported as \"tested\" before, and that is\nnot evidence of anything except that the compiler ran.\n\n## 0. The evidence bar (applies to every app, no exceptions)\n\nFor each app you must produce, and be able to state explicitly:\n\n1. **Version proof** — print/log the actual resolved SDK version from inside the\n   running app (a startup log line, a version screen, an `swift package show-dependencies`\n   / `./gradlew dependencies` / `npm ls @runanywhere/...` output, etc). Never assume\n   a version bump \"took\" just because you edited a manifest.\n2. **Real model acquired** — a real (small) model downloaded fresh, or confirmed\n   already cached, by the app itself (not a file you hand-placed unless that IS the\n   app's supported flow).\n3. **Load** — the model actually loads (watch for a load-succeeded log/state, not\n   just \"no crash\").\n4. **One real inference call** — LLM `generate()` is almost always the easiest\n   modality to drive; use it unless another modality is genuinely less work for\n   that specific app.\n5. **Visible output** — capture the actual generated text (or audio/image) and\n   quote it in your report. \"It responded\" is not enough — show what it said.\n6. **Logs reviewed** — read the console/log stream for errors and warnings and\n   explicitly explain any that appear. Do not silently drop a stack trace because\n   the app \"still produced output.\"\n\nReporting an app as tested from a build/compile success alone is a skill violation.\nIf you could not get past build, that is a BLOCKED report (see §8), not a PASS.\n\n## 1. Locating each app's real local checkout\n\nThis machine accumulates many differently-named copies of the same repos over time\n(worktrees, experiment clones, old branches, scratch dirs). **Never trust a\ndirectory name.** Before touching anything in a candidate directory:\n\n```bash\ngit remote get-url origin          # must be https://github.com/RunanywhereAI/<repo>.git\ngit status --short --branch        # what branch, what's dirty, is it someone else's WIP?\n```\n\nAs of this writing, the known-good local checkouts for this user are here (verify\nthis each time — do not hardcode it further than \"check this location first\"):\n\n```\n~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-ios       -> RunanywhereAI/runanywhere-ios.git\n~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-android   -> RunanywhereAI/runanywhere-android.git\n~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-web       -> RunanywhereAI/runanywhere-web.git\n~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-electron  -> RunanywhereAI/runanywhere-electron.git\n```\n\nThe macOS/Swift case has no separate consumer repo — see §4, it lives inside\n`~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks` (confirm with\n`git remote get-url origin` there too — this machine also has stray full-monorepo\nclones elsewhere, e.g. under `~/development/hard/ambient/`, that are NOT the one to\nuse unless you've verified it's the intended checkout).\n\nThese starter checkouts are commonly sitting on a feature branch, not `main` — e.g.\nat last check `runanywhere-ios` and `runanywhere-android` were on\n`add-gemma4-qwen3.6-qwen3.8-models` and `runanywhere-web` on `add-supertonic-3-tts`.\nThat is fine to test FROM, but say so in your report, and do not `git checkout` away\nfrom a branch that has uncommitted work you didn't create — if `git status --short`\nshows dirty files you don't recognize, stop and ask rather than stashing/resetting.\n\nIf genuinely unsure which directory is real, `git clone` a fresh one into the\nscratchpad rather than guessing — a wrong guess wastes far more time than a clone.\n\n## 2. Cross-repo facts this skill assumes\n\n- SDK version SoT: `core/VERSION` in `runanywhere-sdks`, bumped everywhere by\n  `scripts/release/sync-versions.sh <version>`.\n- Release publish is `.github/workflows/release.yml` (dispatch or `v*.*.*` tag).\n- **Never** treat a build success against `RUNANYWHERE_USE_LOCAL_NATIVES=1` (or any\n  monorepo-local staged artifact) as proof the *public* release works — see §4.\n- QHexRT/QNN material must never appear in a public artifact — release.yml\n  hard-fails on it. If anything you're testing surfaces a QHexRT asset in a public\n  package, stop and flag it, don't route around it.\n\n## 3. iOS — `runanywhere-ios`\n\nSDK pin lives in `Package.swift`:\n\n```swift\n.package(url: \"https://github.com/RunanywhereAI/runanywhere-swift.git\", from: \"0.20.19\")\n```\n\nThis only resolves once `runanywhere-swift` (the generated Swift-only SPM\ndistribution repo — see `~/.claude/skills/sdk-publish/SKILL.md` §4) is\nactually tagged at the target version. Steps:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-ios\n# bump the `from:` version in Package.swift to the target release, then:\nswift package update            # or open in Xcode and let it resolve\n./scripts/build_and_run_ios_sample.sh simulator \"iPhone 16 Pro\"\n```\n\n`./scripts/verify.sh` is the CI-equivalent gate: it resolves the package remotely\nand runs a full `xcodebuild` against the iOS Simulator — use it to prove the\nresolve is clean (it fails if `Package.resolved` ends up dirty, i.e. the committed\npin was stale). `./scripts/smoke.sh` is a fast, non-compiling grep-based check —\nit does NOT count as evidence for this skill's bar, only `build_and_run` +\nwatching real output does.\n\nDrive one real `generate()` call through the app UI (or the equivalent debug\naction if the sample exposes one) against a small downloaded model, then watch the\nruntime log stream for the version line, the model download/load, the generation,\nand any errors:\n\n```bash\nlog stream --predicate 'subsystem CONTAINS \"com.runanywhere\"' --info --debug\n```\n\nMost loggers use `com.runanywhere.RunAnywhereAI`; a few use plain\n`com.runanywhere`; the SDK logs under its own subsystem — the `CONTAINS` match\ncovers all of them. On a physical device use `idevicesyslog | grep \"com.runanywhere\"`\ninstead.\n\n## 4. macOS/Swift — no separate repo, use the in-tree example or rcli\n\nThere is no `runanywhere-macos` consumer repo. Two in-monorepo options, both inside\n`~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks`:\n\n- `bindings/swift/example/` — a minimal SwiftPM example app/executable.\n- `rcli/` — the C++ core's CLI, `rac_*` C ABI consumer.\n\n**The critical gotcha**: the repo's own `Package.swift` is fail-closed toward the\n*real published release* — it resolves the remote `binaryTargets` unless you\nexplicitly opt into locally staged XCFrameworks with\n`RUNANYWHERE_USE_LOCAL_NATIVES=1`. Every doc/script example in this repo sets that\nenv var (because local dev wants fast iteration against in-progress binaries) —\nbut that means copy-pasting those examples verbatim tests your LOCAL staged build,\nnot the actual public release artifact. To prove the published release works:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks/bindings/swift/example\n# DO NOT set RUNANYWHERE_USE_LOCAL_NATIVES — leave it unset so Package.swift\n# resolves the real remote release binaries for the target version.\nswift build\nswift run runanywhere-minimal \"Name three colours.\"\n```\n\nIf the example was previously built/run with the env var set, `swift build` may\nreuse a stale resolve — run `swift package reset` (or delete `.build/`) first if\nyou're not sure which mode the last resolve used.\n\nFor rcli instead (also validates the real published core):\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks\nrcli models download qwen3          # ~639MB, small enough for a quick smoke test\nrcli llm generate --model qwen3 \"Reply with exactly: RCLI WORKS\" --reasoning off\n```\n\n`rcli models list --all` shows the rest of the built-in catalog\n(`whisper-tiny`, `smolvlm2`, `piper`, …) if LLM generate isn't the right modality\nfor what you're verifying.\n\n## 5. Android — `runanywhere-android`\n\nVersion pin: `gradle/libs.versions.toml` has ONE shared catalog entry:\n\n```toml\nrunanywhere = \"0.20.19\"\n...\nrunanywhere-sdk      = { group = \"io.github.sanchitmonga22\", name = \"runanywhere-sdk\",              version.ref = \"runanywhere\" }\nrunanywhere-llamacpp = { group = \"io.github.sanchitmonga22\", name = \"runanywhere-llamacpp\",          version.ref = \"runanywhere\" }\nrunanywhere-onnx     = { group = \"io.github.sanchitmonga22\", name = \"runanywhere-onnx\",               version.ref = \"runanywhere\" }\nrunanywhere-qhexrt   = { group = \"io.github.sanchitmonga22\", name = \"runanywhere-qhexrt-android\",     version.ref = \"runanywhere\" }\n```\n\n**CRITICAL — do not bump this block as one unit.** `runanywhere-qhexrt-android` is\npermanently frozen at whatever version it was last published at before being\nexcluded from the public Maven train; by policy it can never move forward via\npublic Maven again. Verify the actual frozen version FRESH every time (do not\ntrust a remembered number — it doesn't change, but don't skip the check):\n\n```bash\ncurl -s https://repo1.maven.org/maven2/io/github/sanchitmonga22/runanywhere-qhexrt-android/maven-metadata.xml\n```\n\nThe other three artifacts (`runanywhere-sdk`, `-llamacpp`, `-onnx`) CAN and should\nmove to the new release version. This means the single `runanywhere` catalog ref\nmust be **split into two entries** — one at the new version for sdk/llamacpp/onnx,\none pinned to the frozen qhexrt version — not bumped as a block.\n\n**Before trusting a mixed pin works**: verify `RAC_PLUGIN_API_VERSION` (the\nplugin ABI version constant in the C++ core, exposed identically to Kotlin) is\nunchanged between the old public SDK version and the new one. If it changed, mixing\na newer sdk/llamacpp/onnx with the frozen qhexrt version is NOT safe — this is a\nhuman decision (needs either an ABI-compat shim or dropping qhexrt from this build),\nnot something to force through.\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-android\n./gradlew :app:assembleDebug\n./gradlew :app:installDebug        # installs to a connected emulator/device\nadb logcat | grep -i runanywhere   # or a more specific tag if the app uses one\n```\n\nLaunch the app, drive one real `generate()` call against a small downloaded model\n(the app's own model picker/download flow), confirm the resolved SDK version is\nlogged, and review logcat for errors before reporting a PASS.\n\n## 6. Web — `runanywhere-web`\n\n`package.json` has 4 `@runanywhere/*` deps pinned with a caret range:\n\n```json\n\"@runanywhere/proto-ts\":    \"^0.20.19\",\n\"@runanywhere/web\":         \"^0.20.19\",\n\"@runanywhere/web-llamacpp\":\"^0.20.19\",\n\"@runanywhere/web-onnx\":    \"^0.20.19\"\n```\n\nBump all 4 to the new version, then:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-web\nnpm install\nnpm run build\nnpm run dev      # vite --host localhost --port 3000 --strictPort\n```\n\nThe dev/preview server is what supplies COOP/COEP headers required for\n`SharedArrayBuffer` — WASM inference will silently fail (or refuse to init\nthreads) without them, so don't swap in a bare static file server.\n\n`~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks/bindings/web/AGENTS.md`\nhas the authoritative Validation section (its own heading is `## Validation`) —\nfull checklist:\n\n1. Fresh browser context.\n2. Example app served with COOP/COEP headers.\n3. Model download.\n4. Model load.\n5. Real browser inference for the target modality.\n6. Logs/screenshots reviewed.\n\nIts \"Required release gates\" (run before browser validation) from the same repo:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks/bindings/web\nnpm run typecheck && npm run lint && npm run test && npm run build\nnpm run test:browser\nnpm run test:browser:release\ncd example && npm run typecheck && npm run build\n```\n\nStatic gates passing is not the smoke test — you still need an actual browser\nwatching real inference happen against the starter app. Prefer a real browser\nautomation tool (`claude-in-chrome` MCP tools, or Playwright — the maintained\nsuites under `bindings/web/tests/browser/` already use `playwright.config.ts`)\nover curling the served page: WASM inference genuinely needs a browser JS engine.\nLoad the served starter app, download/load a small model, trigger generate, and\nread the actual rendered output text plus the browser console — do not infer\nsuccess from an HTTP 200 on the page load.\n\n## 7. Electron — `runanywhere-electron` (SPECIAL CASE)\n\n**Do not assume a fresh SDK publish is available for this app.** Electron's own\nnpm packages (`@runanywhere/electron`, `-llamacpp`, `-onnx`, `-sherpa`, `-qhexrt`,\n`-neurt`) are on a SEPARATE, manual-publish pipeline — NOT part of `release.yml`\n(grep confirms zero mentions of these package names there). This separation is a\n**confirmed, deliberate** repo-owner decision, not a gap. Check\n`~/.claude/skills/sdk-publish/SKILL.md` §6 before assuming anything moved.\nElectron is **no longer WIP on a branch** — the old \"in-progress on\n`smonga/electron_upgrade`\" note is gone from the root `AGENTS.md`/`CLAUDE.md`\nentirely as of this checkout; `bindings/electron/` is a fully merged SDK on `main`.\nAs of `fix/electron-cicd-staging-url-npu-ane`,\n`.github/workflows/electron-native-package.yml` builds every backend for real and\npackages every tarball — but still does not publish; verify against a live run\nrather than assuming build automation status either way.\n\nTest the starter against whatever version it is CURRENTLY pinned to unless a human\nhas explicitly confirmed a newer Electron publish is ready and intended:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-electron\ncat package.json | grep '@runanywhere/electron'   # confirm current pin before touching it\n```\n\n**`@runanywhere/electron-qhexrt` being public on npm is a CONFIRMED, settled\npolicy decision** (repo owner, 2026-08-20), not a contradiction to flag — it\ncontains real Hexagon NPU binaries (`libQnnHtpV81Skel.so`, `QnnHtp*.dll`,\n`rac_commons.dll`, `runanywhere_qhexrt.dll`) on purpose. The QHexRT-stays-private\npolicy enforced elsewhere in this repo family is specifically about the\nGitHub-Release-side asset set (Android etc.), a different channel. Only re-raise\nthis if you find evidence the policy changed.\n\nTwo layers to actually exercise, do not conflate them:\n\n**(a) Cross-platform Electron JS + macOS native addon** — buildable and testable\nlocally on this Mac right now:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-electron\nnpm install\nnpm run build   # or the app's documented dev/start script — check package.json scripts\nnpm start        # or equivalent — launch the actual Electron app\n```\n\nDrive one real `generate()` call in the running app, watch its console/log output\nfor the resolved SDK version, model download, load, and generation, and read any\nerrors rather than ignoring them.\n\n**(b) Genuine Windows-native behavior** — a macOS build proves NOTHING here\nregardless of how clean the cross-platform code looks; the native addon and any\nWindows-only DLLs must be exercised on real Windows.\n\nThe team keeps a **Windows ARM64 test machine reachable over SSH**. Its hostname,\nuser, key, and VPN/mesh node name are operator-specific and deliberately not\nrecorded in this repo — read them out of the operator's own `~/.ssh/config` (look\nfor the Windows/ARM64 host entry) and use that alias below wherever `$WIN_BOX`\nappears. If no such entry exists, the box is not configured for you: say so and\nask, rather than guessing at a hostname.\n\n```bash\nWIN_BOX=<the ssh alias from ~/.ssh/config>\n```\n\n**Always check reachability first, never just try SSH and retry on timeout.** The\nbox lives on a private mesh VPN and is frequently asleep, so start by asking the\nVPN client whether the node is up (e.g. `tailscale status`, matching on the host\nentry's `HostName`) rather than opening a connection.\n\nIf the node reports `offline, last seen ...`, the box is asleep/powered off.\n**A connection timeout here means \"device asleep,\" not \"config is wrong.\"** Tell\nthe human and ask them to wake it; do not loop retrying SSH. Only once the node\nshows a live/idle state, confirm with:\n\n```bash\nssh -o ConnectTimeout=8 \"$WIN_BOX\" \"whoami\"\n```\n\nThen, with the repo already cloned there:\n\n```bash\nssh \"$WIN_BOX\" \"cd <repo-path-on-that-box>; git pull; npm install; npm run build\"\n# then run whatever launches the Windows Electron build and drive a real generate call\nscp \"$WIN_BOX\":<remote-log-or-artifact-path> ./local-copy   # pull back evidence to inspect\n```\n\nDo not claim Windows-specific validation happened unless you actually obtained a\nshell on that box and ran something real there — a clean macOS build is not\nsubstitute evidence.\n\n**`node_modules/electron` on this box silently tracks whichever arch last ran `npm\ninstall`, not which Node launches the test.** The x64 (llamacpp/onnx/sherpa) and\narm64 (QHexRT) lanes share ONE `runanywhere-electron` checkout/`node_modules` tree.\nPrepending an x64 Node to `PATH` and running `npx playwright test` does NOT make\nElectron itself x64 — `require('electron')` always resolves the single installed\n`node_modules/electron/dist/electron.exe`, and whichever arch that binary actually\nis determines which `prebuilds/win32-<arch>/` directory the running app looks in,\nregardless of `scripts/use-sdk.mjs local`'s staging (which itself is arch-aware\nbased on the *launching* Node, so it can correctly report \"staged win32-x64\" while\nthe process that will actually run is arm64 — a silent mismatch). Verify with:\n`powershell -Command \"$p=[IO.File]::ReadAllBytes('...\\electron.exe'); $peOff=\n[BitConverter]::ToInt32($p,60); [BitConverter]::ToUInt16($p,$peOff+4)\"` — `43620`\n(`0xAA64`) is ARM64, `34404` (`0x8664`) is x64. If it's the wrong arch (e.g. a prior\nsession's `npm install` for the other lane silently overwrote it), a genuine re-test\nof the other lane needs a FULLY SEPARATE checkout/`node_modules` tree installed\nunder the matching-arch Node — don't assume PATH-prepending alone is sufficient, and\ndon't trust `use-sdk.mjs status`'s \"platform: win32-x64\" output as proof the running\nElectron process is actually x64.\n\n**NPU hardware is a scarce, single-owner resource — serialize, never parallelize.**\nThis box has ONE physical Hexagon NPU. `engines/qhexrt/qhexrt_session.cpp`'s\n`session_open()` has a load-bearing comment on this exact failure mode: the HTP\nexposes a small number of protection domains with a per-PD context ceiling, so a\n*second* concurrent model load fails with `RAC_ERROR_BACKEND_INIT_FAILED`\n(\"Backend initialization failed\") — a fast (tens-of-ms), generic-looking failure\nthat is easy to mistake for a real regression. Confirmed once tonight: two agents\neach independently exercising QHexRT on this box at the same time produced exactly\nthis failure. Before concluding a QHexRT test genuinely failed, confirm nothing\nelse (another agent, a leftover Electron process, a stuck previous test run) is\nalso holding an NPU session open on this box — check for lingering `electron.exe`/\n`node.exe` processes from a prior run and kill them if orphaned, and never launch\ntwo NPU-touching verification passes concurrently on this machine.\n\n**\"Backend initialization failed\" has (at least) three distinct root causes —\ndon't stop at PD exhaustion.** Confirmed on 2026-08-17 after PD-exhaustion and\nstaleness were both ruled out (fresh rebuild, source-identical prebuilt, no other\nprocess holding a session): the *actual* bug was two compounding issues, both now\nfixed/documented —\n1. **Real source bug, fixed in PR #733**: `qhexrt_session.cpp`'s `runtime_acquire()`\n   called `qhx_runtime_create(nullptr, nullptr)` on every non-Android platform,\n   trusting the OS default DLL search order to find `QnnHtp.dll`/`QnnSystem.dll`.\n   On Windows that order never includes a dynamically-loaded sibling plugin's own\n   directory (`LOAD_WITH_ALTERED_SEARCH_PATH`, used to load `runanywhere_qhexrt.dll`\n   itself, only widens the search for *that* load call, not later `LoadLibrary`\n   calls made by code already running inside it) — so the QNN DLLs were invisible\n   even sitting right next to the plugin. Fixed by resolving the plugin's own\n   module directory (`GetModuleHandleExA`+`GetModuleFileNameA`) and passing\n   explicit paths, mirroring Android's existing `ADSP_LIBRARY_PATH` resolution in\n   the same file.\n2. **Operational staleness, NOT a script bug**: even after (1), the QNN runtime\n   DLLs actually staged into `@runanywhere/electron-qhexrt`'s\n   `prebuilds/win32-arm64` (`node_modules/.../electron-qhexrt/prebuilds/win32-arm64/`)\n   were from an old QAIRT build (~2.45.x vintage, Dec 2025 files) while the box's\n   installed QAIRT was 2.48.0.260626 — a real ABI-relevant version mismatch.\n   `bindings/electron/scripts/bundle-native.ts`'s `stageQnnRuntime()` just mirrors\n   whatever `RA_QNN_RUNTIME_DIR` points at with **no version check** — it's\n   correct-as-written; someone had pointed it at a stale neurun-side\n   `qairt-runtime-248`-named folder that was never refreshed. Fix: rebuild that\n   flat dir from the CURRENT QAIRT SDK's `lib/aarch64-windows-msvc/` (host DLLs:\n   `QnnHtp.dll`, `QnnHtpPrepare.dll`, `QnnHtpV81Stub.dll`,\n   `QnnHtpV81CalculatorStub.dll`, `QnnSystem.dll`) + `lib/hexagon-v81/unsigned/`\n   (DSP side: `libQnnHtpV81Skel.so`, `libqnnhtpv81.cat`), point\n   `RA_QNN_RUNTIME_DIR` at it, and re-run\n   `npm run bundle:native -- --package=qhexrt` from `bindings/electron/`.\n   **The already-published `@runanywhere/electron-qhexrt` npm package (as of\n   0.20.24) almost certainly ships the same stale DLLs** — this needs a\n   human decision (republish/patch) before claiming the published package works\n   on Windows NPU, separate from the source fix in PR #733.\n3. **PD exhaustion** (the pre-existing entry above) remains real and still applies\n   when two sessions genuinely contend for the one physical NPU — but it is not\n   the only explanation, and the log line to actually check first is the native\n   one: subscribe via `window.runanywhere.logging.records()` (after\n   `logging.setDebugMode(true)`) or add a temporary main-process log capture —\n   `app.process().stdout`/`stderr` do NOT carry it, because the addon runs in a\n   separate `utilityProcess.fork()` host, not the Electron main process, and\n   `RAC_LOG` is not an env var the logger reads (it does nothing). The generic\n   `[QHexRT] qhx_runtime_create failed (QNN libs unavailable?)` vs.\n   `qhx_model_load failed: ... N NPU model(s) already resident` log text is what\n   actually distinguishes cause (1)/(2) from cause (3) — always get that line\n   before diagnosing further.\n\n**Workflow gotcha:** if you orchestrate this via the Workflow tool, its agent()\ncalls do NOT reliably block-wait on a long-running remote build the way a direct\npoll loop does — observed once tonight where a workflow's own agents spawned an\ninternal Monitor/poller for the build log and then returned immediately,\nreporting \"still waiting,\" which let the whole workflow finish with nothing\nactually verified. For \"wait N tens-of-minutes for a real native build to\nfinish,\" use the top-level Monitor tool yourself (a shell poll loop over SSH\nchecking for a terminal BUILD_SUCCEEDED/BUILD_FAILED marker line) rather than\ndelegating the wait itself into a workflow agent's own judgment.\n\n### The Windows box's actual layout, and why it's more than a cross-platform check\n\n**This is a Hexagon-NPU-equipped Windows machine** (Snapdragon-class ARM Windows\ndevice, Copilot+ PC), not just a generic x86 Windows box. That makes it the one\nplace a QHexRT/Hexagon backend build+test can produce real device-only truth for\nWindows — matching the parent workspace's own \"x86 is a simulator, device-only\ntruth\" rule (see the top-level `Qualcomm/CLAUDE.md` and `QHexRT/CLAUDE.md`). Don't\ntreat this box as merely a Windows Electron smoke-test target; it's the only real\nHexagon NPU test rig reachable for this platform.\n\nPer the user (2026-08-17), under `~/Downloads/RunAnywhere/` on that box there are\nthree sibling repo checkouts at the same level — **verify each on connect, don't\nassume paths without checking, since this is relayed secondhand and may have\ndrifted**:\n- `runanywhere-sdks` — this monorepo, presumably already cloned; `git fetch && git\n  log -1 origin/main` to see how far behind it is before doing anything.\n- `runanywhere-electron` — the Electron starter app (same repo as\n  `RunanywhereAI/runanywhere-electron` elsewhere in this skill).\n- a third repo referred to verbally as \"the neuron/new-run inference engine for the\n  Hexagon NPU\" — almost certainly **`neurun`**, the QHexRT/NeuRT split-out repo\n  (see memory: \"neurun component split merged — QHexRT/ + NeuRT/ + shared/, root is\n  a closed 20-entry set enforced by a test\"). Confirm the actual directory name and\n  remote (`git remote get-url origin`) on connect rather than guessing from the\n  transcription.\n\n**Full task on this box (per explicit user instruction, not just a smoke test):**\n1. `git pull` (or fetch+fast-forward) all three repos to latest `main`.\n2. Rebuild the QHexRT/Hexagon NPU backend from `neurun` against this device's real\n   Hexagon NPU — this is the one environment where that build+run is meaningful\n   proof, not a simulator claim.\n3. Bump Electron's native dependencies to their latest versions — llama.cpp,\n   sherpa-onnx, ONNX Runtime, and whatever else `bindings/electron` pins — across\n   the repos involved. Treat this as a real dependency-bump task (check for\n   breaking API changes, don't just edit a version string and assume it builds).\n4. Update the model catalog for **all 5 starter apps across all 4 consumer repos**\n   (iOS, Android, Web, Electron — plus whatever the \"5th app\" split means for\n   macOS/Swift, see the rest of this skill) so the catalog is consistent everywhere,\n   not just on this Windows box.\n5. Run a real end-to-end inference pass on this box specifically (not just build\n   success) — both the general Electron/llama.cpp path AND the Hexagon NPU path via\n   `neurun`/QHexRT.\n6. Open PRs for whatever changed, in each affected repo. Do not merge them —\n   the user explicitly said they'll review.\n\nThis is real engineering work (native dependency bumps + an NPU backend rebuild),\nnot a mechanical version-string edit — expect it to take real time and to\ngenuinely need `QHexRT/CLAUDE.md` and `neurun`'s own docs read in full before\ntouching that backend, per the parent workspace's own instructions.\n\n## 8. Reporting\n\nFor each of the 5 apps (iOS, macOS/Swift, Android, Web, Electron), state exactly\none of these two outcomes — never blur them:\n\n- **FULLY VERIFIED** — with concrete evidence: the version string actually\n  resolved/logged, the model used, and the actual output produced (quote it).\n- **BLOCKED** — with the exact concrete reason, e.g.: the Windows ARM64 box's\n  mesh-VPN node is offline; Android emulator unavailable in this environment; App Store /\n  notarization credential missing; `runanywhere-swift` not yet tagged at target\n  version so `swift package update` can't resolve; electron-qhexrt policy conflict\n  needs a human call before proceeding further.\n\nA clearly reported partial/blocked result is far more valuable than an optimistic\nsummary that overstates what was actually observed. If you hit the known Electron\nqhexrt npm-publish policy conflict (§7), call it out in the report even if it\nwasn't the thing blocking your specific test — it's a standing issue every release\nnight should re-surface until a human resolves it.\n\n## Credentials referenced but never printed by this skill\n\nThese exist and are checked for presence only — never print their values:\n\n- Maven Central: `mavenCentral.username` / `mavenCentral.password`,\n  `signing.gnupg.keyName` / `signing.gnupg.passphrase` in `~/.gradle/gradle.properties`.\n- GPG signing key `CC377A9928C7BB18` — check with\n  `gpg --list-secret-keys --keyid-format LONG`.\n- macOS notarization profile `runanywhere-notary` — check with\n  `xcrun notarytool history --keychain-profile runanywhere-notary`; codesign\n  identity `Developer ID Application: RunAnywhere, Inc` — check with\n  `security find-identity -v -p codesigning`.\n- npm — `npm whoami` should already show a logged-in user.\n\nNone of the app-level smoke tests in this skill require touching these directly;\nthey matter only if a test path requires a fresh distro-repo publish (e.g.\n`runanywhere-swift` not yet tagged) — that's a separate concern of the\n`sdk-publish` skill, not this one.\n","frontmatter":{"name":"sdk-test-starters","description":"Run a REAL end-to-end inference smoke test (model download + load + generate, with visible output) on every RunAnywhere starter app — iOS, macOS/Swift, Android, Web, Electron — after a runanywhere-sdks release. Build success alone is never sufficient evidence."},"isInternal":false,"tokens":7370,"sizeBytes":28933},{"name":"SKILL.md","path":".claude/skills/telemetry-e2e/SKILL.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/.claude/skills/telemetry-e2e/SKILL.md","title":"telemetry-e2e","category":"anthropic-skill","format":"markdown","content":"---\nname: telemetry-e2e\ndescription: Prove that what an SDK binding EMITS on the telemetry wire is correct and that it actually arrives — vocabulary compliance, presence/absence semantics, device-id stability, and a received request body / stored row as evidence. Use when touching telemetry_json.cpp, telemetry_manager.cpp, rac_telemetry_vocabulary.h, any binding's platform-adapter telemetry bridge, or when asked to verify a telemetry field isn't silently dropping.\n---\n\n# Telemetry Wire Correctness (SDK side)\n\nThis is the SDK-side counterpart to the backend's `telemetry-e2e` skill (private\n`Runanywhere-monorepo` repo — proves event-to-stored-row on the server side). This one\nproves the other half: that commons *serializes* a call correctly and that the bytes\n*leaving* a binding are the bytes a real backend accepts. It does not cover per-starter-app\ninference smoke tests — see `sdk-test-starters` for that.\n\n## 0. The evidence bar\n\nCompiling is not evidence. A green `telemetry_extraction_tests` run is not evidence that a\nreal binding's real call site reaches the wire correctly — it only proves the *serializer*\nhandles a hand-built payload correctly. In order of increasing strength:\n\n1. **Unit test green** (`ctest -R telemetry_extraction_tests`) — proves the JSON builder's\n   presence/vocabulary rules are still correct in isolation. Necessary, not sufficient.\n2. **Received request body** — capture the actual JSON `rcli` (or a real binding) put on\n   the wire and read it: right modality endpoint, right keys, no vocabulary values that\n   got through that shouldn't have, measured zeros kept as `0` not `null`.\n3. **Stored row matches, field-for-field** — the backend's own ingest response\n   (`events_received`/`events_stored`/`events_skipped`) or a direct DB read shows the row\n   landed with the values you actually sent, not just a 2xx. This is the only tier that\n   also proves the vocabulary/schema on both sides still agree.\n\nA PR description that says \"sent telemetry, no errors\" without quoting a request body or a\nstored-row count is not evidence of anything except that a socket didn't error out.\n\n## 1. The emit path\n\nEvery binding funnels through the same two C ABI entry points in\n`core/src/infrastructure/telemetry/telemetry_manager.cpp`:\n\n- `rac_telemetry_manager_track` — flat-struct payload (`rac_telemetry_payload_t`), used by\n  `rcli`'s `telemetry emit`/`telemetry blast` and by hand-built events.\n- `rac_telemetry_manager_track_proto` — the real path every language binding actually uses:\n  a binding builds a protobuf `SDKEvent` (`sdk_events.pb.h`) at the real call site (e.g. an\n  LLM `generate()` completing), and `proto_event_type_string()` + the per-domain extraction\n  blocks in `telemetry_manager.cpp` turn it into the same flat payload. `framework_proto_to_string()`\n  / `framework_to_string()` / `clean_framework()` do the backend-name normalization here —\n  this is where a raw enum like `INFERENCE_FRAMEWORK_LLAMA_CPP` becomes the wire string `\"llamacpp\"`.\n- `stamp_live_device_state()` (same file) then overwrites the SDK-origin/device-state fields\n  (`sdk_binding`, `platform`, `battery_state`, memory, core count) from manager-owned state,\n  not from whatever the caller passed — a binding cannot inject a platform or binding value.\n- The payload is handed to `rac_telemetry_manager_payload_to_json()` in\n  `core/src/infrastructure/telemetry/telemetry_json.cpp`, which does field-by-field\n  serialization (see §2/§3 for the two contracts enforced there).\n- `send_batch_json()` batches by modality and POSTs to `/api/v2/sdk/telemetry/{modality}`\n  through whichever `rac_http_transport_ops_t` the binding registered (URLSession/OkHttp/\n  `emscripten_fetch`/curl for `rcli` — see the root `AGENTS.md`'s \"HTTP is platform-provided\"\n  rule). There is no `libcurl` fallback baked into commons itself.\n\nIf a field is going missing on the wire, the fix is almost always in\n`telemetry_json.cpp` (a field never wired into the serializer) or in the call site's proto\npopulation (a field never set on the `SDKEvent`) — not in a binding's bridge code, which\njust forwards bytes.\n\n## 2. The closed vocabularies — and the silent-drop rule\n\n`core/include/rac/infrastructure/telemetry/rac_telemetry_vocabulary.h` is a **generated\nfile** (see §5) holding six closed tables, each with a `RAC_TELEMETRY_<NAME>_COUNT` and a\n`RAC_TELEMETRY_<NAME>_VALUES[]` array:\n\n| Table | Count | Wire field | Values |\n|---|---|---|---|\n| `RAC_TELEMETRY_EVENT_TYPE` | 106 | `event_type` | namespaced strings, e.g. `llm.generation.completed`, `auth.succeeded`, `stt.transcription.completed` |\n| `RAC_TELEMETRY_BACKEND` | 22 | `backend` / `framework` | `onnx, sherpa, llamacpp, foundation_models, system_tts, fluid_audio, coreml, whisperkit_coreml, mlx, qhexrt, neurt, tflite, executorch, mediapipe, mlc, pico_llm, piper_tts, swift_transformers, cloud, builtin, none, unknown` |\n| `RAC_TELEMETRY_MODEL_SOURCE` | 3 | `model_source` | `catalog, builtin, user` |\n| `RAC_TELEMETRY_PLATFORM` | 9 | `platform` | `ios, android, macos, windows, linux, web, tvos, watchos, visionos` |\n| `RAC_TELEMETRY_SDK_BINDING` | 9 | `sdk_binding` | `swift, kotlin, flutter, react-native, web, electron, python, cli, cpp` |\n| `RAC_TELEMETRY_BATTERY_STATE` | 4 | `battery_state` | `charging, full, unplugged, unknown` |\n\n**The rule, verbatim from `add_vocabulary_string()` in `telemetry_json.cpp`:** if a value is\nnon-null but not present in its table, the field is dropped to `null` and the *rest of the\nevent still ships*. This is by design (a bad `backend` string must not cost the whole\nobservation) but it means:\n\n- The backend never sees the bad value, never 422s, never quarantines anything — there is\n  no error path anywhere for this. A field just isn't there.\n- The only client-side signal is a `RAC_LOG_WARNING(\"Telemetry\", \"Dropping %s: '%s' is not\n  in the published vocabulary\", ...)` log line — easy to miss unless you're specifically\n  watching debug logs.\n- `platform`, `sdk_binding`, and `battery_state` are stamped by the manager itself (§1), not\n  supplied by a binding's call site, so in practice only `backend`/`framework` (from the\n  engine/model layer) and `event_type` (only reachable via the proto path, checked at CI\n  time — see §5) are realistic places a new value can silently disappear.\n- `test_telemetry_extraction.cpp`'s vocabulary block (`\"vocab-1\"`/`\"vocab-2\"` cases) is the\n  concrete regression guard: it sends `framework = \"LlamaCpp\"` (a real spelling that used to\n  reach production) and asserts the string never appears on the wire while `model_id` still\n  does. If you touch `add_vocabulary_string()` or add a new vocabulary-gated field, extend\n  that test rather than trusting a manual check.\n\nHistorical motivation (from the header's own comment): before this table existed, stored\nproduction rows had four spellings of `llama.cpp`, three of ONNX, and binding names like\n`\"react-native\"` recorded as a `platform` — this file exists specifically to make that class\nof drift fail in this repo instead of being discovered in a database nobody was watching.\n\n## 3. Presence vs. absence: the \"measured zero\" contract\n\nBefore the fix documented in `telemetry_json.cpp`'s header, extractors read proto3 scalar\nfields directly — which report `0` for a field that was never set — and the old serializer\n(`add_int`/`add_double`) *skipped* zero values to keep payloads small. Result: a genuinely\nmeasured `output_tokens = 0` (an empty completion) and an unmeasured `tokens_per_second`\nboth arrived on the wire as absent, indistinguishable from each other.\n\nThe fix is `has_x`-gated serialization. Two helper families in `telemetry_json.cpp`:\n\n- `add_int_or_null(key, value, is_valid)` / `add_double_or_null(key, value, is_valid)` —\n  emit the real value (including `0`) when `is_valid` is true, else the JSON literal `null`.\n  Non-finite doubles (`NaN`/`Inf`) always degrade to `null` regardless of `is_valid`, because\n  JSON has no such literal and one bad value must not corrupt the whole batch.\n- The `is_valid` argument is a real `has_x`/sentinel check per field, not a blanket flag —\n  e.g. `payload->has_processing_time_ms != RAC_FALSE`, `payload->total_memory > 0`,\n  `payload->battery_level >= 0`.\n\n**What to check to confirm this stayed fixed** (this is exactly what\n`test_telemetry_extraction.cpp`'s presence block does — see §4): send an LLM completion\nevent with `output_tokens = 0` and `input_tokens = 0` explicitly set, and\n`tokens_per_second`/`context_length` left unset. Assert the wire body contains\n`\"output_tokens\":0` and `\"input_tokens\":0` as literal numbers, and `\"tokens_per_second\":null`\n/ `\"context_length\":null` — not all four as `null`, and not all four as `0`. If a future\nrefactor reverts to plain `add_int`/`add_double` for any of these fields, that regression\ntest is the thing that catches it; grep `telemetry_json.cpp` for `add_int(` / `add_double(`\n(the non-`_or_null`/non-`_always` forms) if you suspect a field regressed — those two still\nsilently skip zero by design and must never be used for a field where `0` is meaningful.\n\n## 4. Build, test, and what each telemetry test asserts (macOS)\n\n```bash\ncmake --preset macos-debug\ncmake --build build/macos-debug\nctest --preset macos-debug --output-on-failure -R 'telemetry|device_identity'\n```\n\nOr the non-preset form (`core/AGENTS.md`'s documented path):\n\n```bash\ncmake -B build -DRAC_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Debug\ncmake --build build\nctest --test-dir build --output-on-failure\n```\n\n`telemetry_extraction_tests` and `device_identity_tests` are registered in\n`core/tests/CMakeLists.txt`; `rcli_telemetry_live_tests` is registered separately in\n`rcli/tests/CMakeLists.txt`. All three are hand-rolled assertion runners — `CHECK()` in\nsome files, `ASSERT_EQ`/`ASSERT_TRUE` (from `core/tests/test_common.h`) in others — never\nGoogleTest:\n\n| ctest name | Binary | What it asserts |\n|---|---|---|\n| `telemetry_extraction_tests` | `test_telemetry_extraction` | Full proto→JSON pipeline per modality (LLM token counts/`tokens_per_second`, STT NaN-confidence never leaks as the string `\"nan\"`, TTS `characters_per_second`, embeddings `total_tokens`/`batch_size`/`embedding_dimension`, LoRA failure-path fields, RAG retrieval counts, VLM image/timing fields, live device-state stamping); the presence/measured-zero contract (§3); the closed-vocabulary drop rule (§2); and an HTTP-retry backoff (a failed batch is re-sent only after >5s, never before). Requires `RAC_HAVE_PROTOBUF` — the test binary itself no-ops with exit 0 if commons wasn't built with protobuf support. |\n| `device_identity_tests` | `test_device_identity` | Every branch of `rac_device_get_or_create_persistent_id()`'s resolution chain (§6) against a mocked platform adapter — cache hit, vendor-ID fallback, fresh-UUID generation, and that a `secure_set` failure doesn't silently drop the ID. |\n| `rcli_telemetry_live_tests` | `test_rcli_telemetry_live` | **Opt-in, always registered but a no-op by default.** Runs only with `--live` *and* `RUNANYWHERE_BASE_URL`/`RUNANYWHERE_API_KEY` set; otherwise prints \"skip\" and exits 0 — a green ctest run for this target proves nothing by itself. When live, it runs `rcli::bootstrap()` (real API-key → device-register → JWT handshake), sends one real event per modality (LLM, embeddings, RAG, VLM, LoRA-failure) through the real HTTP transport, and fails on any non-2xx response — this is the test that would catch a strict-schema `422 extra_forbidden` from field drift against the real V2 endpoints. |\n\nRun it live (§7 has the full local-backend flow):\n\n```bash\nRUNANYWHERE_BASE_URL=https://<backend-origin> RUNANYWHERE_API_KEY=<key> \\\n  ./build/macos-debug/rcli/tests/test_rcli_telemetry_live --live\n```\n\n`test_platform_bridge` (same `CMakeLists.txt`, adjacent target) is the wider companion\ncheck — \"every value a binding supplies must reach the wire\" — covering the platform half\nof the same two flat C structs (`rac_client_info_t`/`rac_device_registration_info_t`) for\nevery SDK at once; run it alongside if you're touching client-info fields rather than\ntelemetry event fields specifically.\n\n## 5. Regenerating the vocabulary header, and the drift gate\n\n`rac_telemetry_vocabulary.h` (§2) is generated from `idl/http/sdk-openapi.json` — the\ndevice-facing OpenAPI subset copied in from the private backend repo. **How that file gets\nupdated on the backend side, and how it crosses into this repo, is the monorepo's\n`openapi-contract` skill's job — read that by name in the other repo rather than\nre-deriving the steps here.** From inside this repo, once `idl/http/sdk-openapi.json` is\ncurrent:\n\n```bash\npython3 idl/codegen/generate_telemetry_vocabulary.py          # regenerate the header\npython3 idl/codegen/generate_telemetry_vocabulary.py --check  # CI drift check; exit 1 if stale\n```\n\n`.github/workflows/telemetry-vocabulary-drift.yml` runs three checks on every push to\n`main`/`development` and on any PR touching `idl/http/**`, the codegen script, or the\ntelemetry source/header dirs:\n\n1. **Generated header is current** — the `--check` command above.\n2. **Every event type the core can emit is in the vocabulary** — a Python check that parses\n   `proto_event_type_string()`'s body out of `telemetry_manager.cpp` via regex, unions in\n   the two prefix-expansion families (`stt.model.*`/`tts.voice.*`/`llm.model.*` and\n   `llm*`/`vlm*` generation suffixes), and diffs against `TelemetryEventType`'s enum in\n   `idl/http/sdk-openapi.json`. Anything emittable but unpublished fails the job.\n3. **Every framework string the core can emit is in the vocabulary** — same technique\n   against `framework_proto_to_string()` and `framework_to_string()` vs. `TelemetryBackend`.\n\n**A documented drift you will hit if you trust the wrong file**: `idl/http/README.md`'s own\n\"What it pins\" table names the schema for the `framework`/`backend` field as\n`TelemetryFramework` — that's stale. The actual generator\n(`idl/codegen/generate_telemetry_vocabulary.py`'s `VOCABULARIES` dict) and the header it\nproduces both say `TelemetryBackend`. Trust the generator and the generated header's own\n`// Published as X` comments over that README table.\n\n## 6. Per-binding device identity: where it's created, where it's persisted\n\nThe resolution chain itself is centralized in commons —\n`core/src/infrastructure/device/device_identity.cpp`\n(`rac_device_get_or_create_persistent_id`, tested by `device_identity_tests`, §4) — and is\nthe same fixed order for every binding:\n\n1. `adapter.secure_get(\"com.runanywhere.sdk.device.uuid\")` — if found and non-empty, use it,\n   full stop.\n2. `adapter.get_vendor_id` (if the platform adapter provides one) — use it, then persist it\n   via `secure_set` so step 1 hits next boot.\n3. Generate a fresh RFC-4122 v4 UUID, persist it via `secure_set`.\n\nWhat differs per binding is **only** the `secure_get`/`secure_set` backing store — an\nunstable backing store (one that doesn't survive a real app restart, or that resolves to a\ndifferent physical location than the read path expects) is exactly the bug class that\ninflates device counts, because step 3 fires again on every \"cache miss\":\n\n| Binding | Backing store | Where |\n|---|---|---|\n| Swift (iOS/macOS) | Keychain, service `com.runanywhere.sdk` | `bindings/swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+PlatformAdapter.swift` (`platformSecureGetCallback`/`platformSecureSetCallback`) |\n| Kotlin (Android) | Android Keystore AES-256-GCM key, ciphertext in a no-backup app file (not `SharedPreferences`) | `bindings/kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/security/AndroidKeychainManager.kt` (`NoBackupCiphertextStore`), wired through `CppBridgePlatformAdapter.PlatformSecureStorage` |\n| Flutter | Native Keychain/Keystore helper via FFI symbols `ra_flutter_secure_storage_{store,retrieve,delete}` | `bindings/flutter/packages/runanywhere/lib/native/dart_bridge_secure_storage.dart`; the device-UUID key constant is duplicated (deliberately, per its own comment) as `_keyDeviceUUID = 'com.runanywhere.sdk.device.uuid'` in `dart_bridge_device.dart` |\n| React Native (Android) | `SecureStorageManager` (native Android side) via `PlatformAdapterBridge.kt`'s `secureGet`/`secureSet` | `bindings/react-native/packages/core/android/src/main/java/com/margelo/nitro/runanywhere/PlatformAdapterBridge.kt` |\n| Web | `localStorage`, key `rac_sdk_plaintext_com.runanywhere.sdk.device.uuid` — **plaintext**, no OS-level encryption equivalent exists in a browser; restricted by convention to non-sensitive IDs | `bindings/web/packages/core/src/runtime/PlatformAdapter.ts` (`registerSecureGet`/`registerSecureSet`); also has a separate `stableVendorId()` fallback under key `rac_sdk_plaintext_vendor_id` |\n| Electron (macOS/Linux) | Flat file under a secure directory, path-traversal-guarded (`fs::path(key)` must not be absolute or contain `..`) | `bindings/electron/native/posix_platform_adapter.cpp` (`posix_secure_get`/`posix_secure_set`) |\n| Electron (Windows) | Windows DPAPI (`CryptProtectData`, current-user scope, no UI) | `bindings/electron/native/win32_platform_adapter.cpp` (`win_secure_get`/`win_secure_set`) |\n\nThe key string itself — `com.runanywhere.sdk.device.uuid` — is intentionally the same\nliteral across Swift, Flutter, and RN (each source comments cross-reference the other two by\nname), so grepping for it is a fast way to confirm a new binding is reading/writing the\n*same* key it thinks it is, not a typo'd sibling.\n\n**How to verify stability across a real restart** (not just \"the code looks right\"):\n\n1. Run the binding once, capture the emitted `device_id` from a real request body (§7) or\n   from `POST /api/v1/devices/register`'s response.\n2. Fully terminate the process (not just background it — on mobile this matters, a\n   backgrounded app can keep its in-memory device-id cache alive without ever touching\n   the read path again).\n3. Relaunch and repeat step 1.\n4. The `device_id` must be byte-identical. If it changed, the break is almost always one of:\n   the backing store not surviving process death (e.g. an in-memory-only stub used during\n   development), a key-name mismatch between the write and read paths, or (Android\n   specifically) `Context.deleteSharedPreferences` being invoked on the wrong file — see\n   `AndroidKeychainManager.kt`'s constructor, which deletes a hardcoded *legacy* prefs name\n   on every construction; if that name were ever widened to match the real store, every\n   Android launch would look like a fresh device.\n\nTwo-in-one launches on the *same physical device* (e.g. two emulator instances, or a\nsame-device reinstall vs. a real fresh install) are a different, real-world failure mode\nfrom a code bug — don't conflate an intentional reinstall's fresh ID with instability.\n\n## 7. Point a local build at a local backend and prove an event landed\n\n`rcli` is the fastest way to exercise the full wire path without a mobile/browser\ntoolchain — it links `rac_commons` directly and speaks real HTTP via curl.\n\n```bash\ncmake --preset rcli-macos-release   # or rcli-linux-release on Linux\ncmake --build build/rcli-macos-release --target rcli -j \"$(sysctl -n hw.logicalcpu)\"\n```\n\n**Keyless (development environment) — no API key, works against any backend that allows\nanonymous V2 telemetry:**\n\n```bash\n./build/rcli-macos-release/rcli/rcli --environment development \\\n  --base-url http://127.0.0.1:8000 \\\n  telemetry blast --processing-ms 42.5 --session-id my-check \\\n  --input-tokens 128 --output-tokens 256\n```\n\nThis is exactly the shape `scripts/ci/oss_keyless_telemetry_blast.sh` runs in CI against the\npublic staging backend — `--environment development --base-url <origin> telemetry blast\n...` with no `--api-key`. `telemetry blast` sends one representative event per modality (all\n12) and prints a `MODALITY | RESULT | STATUS | RECEIVED | STORED | SKIPPED` table parsed\ndirectly from the backend's own batch response — **the `STORED` column is authoritative\nserver-side confirmation**, not just an HTTP 2xx. `run_telemetry_blast()`'s own exit-code\nlogic (`rcli/src/commands/cmd_telemetry.cpp`) does fold `stored >= count` into its overall\npass/fail, so a genuine storage shortfall on any modality does make the process exit\nnon-zero, not just a transport error — but the exit code is one bit for all 12 modalities\ncombined, so a non-zero exit still requires reading the table to know *which* modality\nfailed and *why* (a `network error` status is a transport problem; an `HTTP 200` row with\n`STORED` under `RECEIVED` is a row-level rejection, e.g. a vocabulary value the backend\ndidn't recognize — a real bug to chase, not the same failure class at all).\n\n**Authenticated (production shape) — for exercising the login handshake too:**\n\n```bash\n./build/rcli-macos-release/rcli/rcli --environment production \\\n  --base-url https://<backend-origin> --api-key <key> \\\n  auth login\n./build/rcli-macos-release/rcli/rcli --environment production \\\n  --base-url https://<backend-origin> --api-key <key> \\\n  telemetry emit --modality llm --count 3 --input-tokens 128 --output-tokens 256\n```\n\n`auth login` only has a path in `production` — `development`'s keyless mode has no login\nstep by design (see `rcli/src/commands/cmd_auth.cpp`'s own subcommand help text).\n\n**To inspect the actual request body**, not just the summary table, add `--json` for\nmachine-readable CLI output and `-v` for debug logging (the debug log includes the\nserialized JSON per batch before it goes on the wire) — or point `--base-url` at a throwaway\nlocal capture endpoint (any script that logs the raw POST body) before pointing it at a real\nbackend, to sanity-check field shape offline first.\n\n**If you have the private backend repo checked out locally too**, running it yourself\n(`cd backend && uv run uvicorn main:app --reload`, per that repo's own README) and pointing\n`--base-url` at `http://127.0.0.1:8000` gets you the strongest possible evidence: query the\nSQLite/Postgres row directly after a blast and diff it field-for-field against what you sent\n— the monorepo's own `telemetry-e2e` skill (its integration suite, hermetic pytest fixtures,\nand `test_telemetry_conformance.py`'s vocabulary/presence/identity assertions against stored\nrows) is the authoritative reference for that side; don't re-implement its DB assertions\nhere, just point at it.\n\n## Evidence bar, restated\n\n- Compiling `rcli` or a binding: **not evidence**.\n- `ctest -R telemetry_extraction_tests` green: **not evidence of a real call site** — only\n  of the serializer in isolation.\n- `rcli telemetry blast` exiting 0: real signal (its exit code does fold in the backend's\n  `stored >= count` check, §7) but still worth pairing with the per-modality\n  `STORED`/`SKIPPED` table in your report — the exit code can't tell you *which* modality\n  or *why* a shortfall happened.\n- A captured request body showing the right endpoint, the right keys, correct\n  presence/absence per §3, and no off-vocabulary values per §2 — **evidence of correct\n  emission**.\n- That request body's values matching a stored row (via the backend's own\n  `events_stored` count at minimum, a direct DB read at best) — **evidence of correct\n  end-to-end delivery**, the only tier worth reporting as \"telemetry works.\"\n","frontmatter":{"name":"telemetry-e2e","description":"Prove that what an SDK binding EMITS on the telemetry wire is correct and that it actually arrives — vocabulary compliance, presence/absence semantics, device-id stability, and a received request body / stored row as evidence. Use when touching telemetry_json.cpp, telemetry_manager.cpp, rac_telemetry_vocabulary.h, any binding's platform-adapter telemetry bridge, or when asked to verify a telemetry field isn't silently dropping."},"isInternal":false,"tokens":5937,"sizeBytes":23363},{"name":"package-lock.json","path":"bindings/electron/packages/llamacpp/package-lock.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/llamacpp/package-lock.json","title":"Llamacpp Skill","category":"anthropic-skill","format":"json","content":"# Llamacpp Skill\n\nPath: `bindings/electron/packages/llamacpp/package-lock.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/llamacpp/package-lock.json)","tokens":300,"sizeBytes":234},{"name":"package.json","path":"bindings/electron/packages/llamacpp/package.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/llamacpp/package.json","title":"Llamacpp Skill","category":"anthropic-skill","format":"json","content":"# Llamacpp Skill\n\nPath: `bindings/electron/packages/llamacpp/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/llamacpp/package.json)","tokens":300,"sizeBytes":224},{"name":"tsconfig.json","path":"bindings/electron/packages/llamacpp/tsconfig.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/llamacpp/tsconfig.json","title":"Llamacpp Skill","category":"anthropic-skill","format":"json","content":"# Llamacpp Skill\n\nPath: `bindings/electron/packages/llamacpp/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/llamacpp/tsconfig.json)","tokens":300,"sizeBytes":226},{"name":"package-lock.json","path":"bindings/electron/packages/neurt/package-lock.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/neurt/package-lock.json","title":"Neurt Skill","category":"anthropic-skill","format":"json","content":"# Neurt Skill\n\nPath: `bindings/electron/packages/neurt/package-lock.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/neurt/package-lock.json)","tokens":300,"sizeBytes":225},{"name":"package.json","path":"bindings/electron/packages/neurt/package.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/neurt/package.json","title":"Neurt Skill","category":"anthropic-skill","format":"json","content":"# Neurt Skill\n\nPath: `bindings/electron/packages/neurt/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/neurt/package.json)","tokens":300,"sizeBytes":215},{"name":"tsconfig.json","path":"bindings/electron/packages/neurt/tsconfig.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/neurt/tsconfig.json","title":"Neurt Skill","category":"anthropic-skill","format":"json","content":"# Neurt Skill\n\nPath: `bindings/electron/packages/neurt/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/neurt/tsconfig.json)","tokens":300,"sizeBytes":217},{"name":"package-lock.json","path":"bindings/electron/packages/onnx/package-lock.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/onnx/package-lock.json","title":"Onnx Skill","category":"anthropic-skill","format":"json","content":"# Onnx Skill\n\nPath: `bindings/electron/packages/onnx/package-lock.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/onnx/package-lock.json)","tokens":300,"sizeBytes":222},{"name":"package.json","path":"bindings/electron/packages/onnx/package.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/onnx/package.json","title":"Onnx Skill","category":"anthropic-skill","format":"json","content":"# Onnx Skill\n\nPath: `bindings/electron/packages/onnx/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/onnx/package.json)","tokens":300,"sizeBytes":212},{"name":"tsconfig.json","path":"bindings/electron/packages/onnx/tsconfig.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/onnx/tsconfig.json","title":"Onnx Skill","category":"anthropic-skill","format":"json","content":"# Onnx Skill\n\nPath: `bindings/electron/packages/onnx/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/onnx/tsconfig.json)","tokens":300,"sizeBytes":214},{"name":"package-lock.json","path":"bindings/electron/packages/qhexrt/package-lock.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/qhexrt/package-lock.json","title":"Qhexrt Skill","category":"anthropic-skill","format":"json","content":"# Qhexrt Skill\n\nPath: `bindings/electron/packages/qhexrt/package-lock.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/qhexrt/package-lock.json)","tokens":300,"sizeBytes":228},{"name":"package.json","path":"bindings/electron/packages/qhexrt/package.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/qhexrt/package.json","title":"Qhexrt Skill","category":"anthropic-skill","format":"json","content":"# Qhexrt Skill\n\nPath: `bindings/electron/packages/qhexrt/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/qhexrt/package.json)","tokens":300,"sizeBytes":218},{"name":"tsconfig.json","path":"bindings/electron/packages/qhexrt/tsconfig.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/qhexrt/tsconfig.json","title":"Qhexrt Skill","category":"anthropic-skill","format":"json","content":"# Qhexrt Skill\n\nPath: `bindings/electron/packages/qhexrt/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/qhexrt/tsconfig.json)","tokens":300,"sizeBytes":220},{"name":"package-lock.json","path":"bindings/electron/packages/sherpa/package-lock.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/sherpa/package-lock.json","title":"Sherpa Skill","category":"anthropic-skill","format":"json","content":"# Sherpa Skill\n\nPath: `bindings/electron/packages/sherpa/package-lock.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/sherpa/package-lock.json)","tokens":300,"sizeBytes":228},{"name":"package.json","path":"bindings/electron/packages/sherpa/package.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/sherpa/package.json","title":"Sherpa Skill","category":"anthropic-skill","format":"json","content":"# Sherpa Skill\n\nPath: `bindings/electron/packages/sherpa/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/sherpa/package.json)","tokens":300,"sizeBytes":218},{"name":"tsconfig.json","path":"bindings/electron/packages/sherpa/tsconfig.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/sherpa/tsconfig.json","title":"Sherpa Skill","category":"anthropic-skill","format":"json","content":"# Sherpa Skill\n\nPath: `bindings/electron/packages/sherpa/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/sherpa/tsconfig.json)","tokens":300,"sizeBytes":220},{"name":"CHANGELOG.md","path":"bindings/flutter/packages/runanywhere_llamacpp/CHANGELOG.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_llamacpp/CHANGELOG.md","title":"Runanywhere_llamacpp Skill","category":"anthropic-skill","format":"markdown","content":"# Runanywhere_llamacpp Skill\n\nPath: `bindings/flutter/packages/runanywhere_llamacpp/CHANGELOG.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_llamacpp/CHANGELOG.md)","tokens":300,"sizeBytes":258},{"name":"pubspec.yaml","path":"bindings/flutter/packages/runanywhere_llamacpp/pubspec.yaml","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_llamacpp/pubspec.yaml","title":"Runanywhere_llamacpp Skill","category":"anthropic-skill","format":"yaml","content":"# Runanywhere_llamacpp Skill\n\nPath: `bindings/flutter/packages/runanywhere_llamacpp/pubspec.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_llamacpp/pubspec.yaml)","tokens":300,"sizeBytes":258},{"name":"CHANGELOG.md","path":"bindings/flutter/packages/runanywhere_mlx/CHANGELOG.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_mlx/CHANGELOG.md","title":"Runanywhere_mlx Skill","category":"anthropic-skill","format":"markdown","content":"# Runanywhere_mlx Skill\n\nPath: `bindings/flutter/packages/runanywhere_mlx/CHANGELOG.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_mlx/CHANGELOG.md)","tokens":300,"sizeBytes":243},{"name":"pubspec.yaml","path":"bindings/flutter/packages/runanywhere_mlx/pubspec.yaml","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_mlx/pubspec.yaml","title":"Runanywhere_mlx Skill","category":"anthropic-skill","format":"yaml","content":"# Runanywhere_mlx Skill\n\nPath: `bindings/flutter/packages/runanywhere_mlx/pubspec.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_mlx/pubspec.yaml)","tokens":300,"sizeBytes":243},{"name":"CHANGELOG.md","path":"bindings/flutter/packages/runanywhere_onnx/CHANGELOG.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_onnx/CHANGELOG.md","title":"Runanywhere_onnx Skill","category":"anthropic-skill","format":"markdown","content":"# Runanywhere_onnx Skill\n\nPath: `bindings/flutter/packages/runanywhere_onnx/CHANGELOG.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_onnx/CHANGELOG.md)","tokens":300,"sizeBytes":246},{"name":"pubspec.yaml","path":"bindings/flutter/packages/runanywhere_onnx/pubspec.yaml","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_onnx/pubspec.yaml","title":"Runanywhere_onnx Skill","category":"anthropic-skill","format":"yaml","content":"# Runanywhere_onnx Skill\n\nPath: `bindings/flutter/packages/runanywhere_onnx/pubspec.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_onnx/pubspec.yaml)","tokens":300,"sizeBytes":246},{"name":"CHANGELOG.md","path":"bindings/flutter/packages/runanywhere_qhexrt/CHANGELOG.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_qhexrt/CHANGELOG.md","title":"Runanywhere_qhexrt Skill","category":"anthropic-skill","format":"markdown","content":"# Runanywhere_qhexrt Skill\n\nPath: `bindings/flutter/packages/runanywhere_qhexrt/CHANGELOG.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_qhexrt/CHANGELOG.md)","tokens":300,"sizeBytes":252},{"name":"pubspec.yaml","path":"bindings/flutter/packages/runanywhere_qhexrt/pubspec.yaml","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_qhexrt/pubspec.yaml","title":"Runanywhere_qhexrt Skill","category":"anthropic-skill","format":"yaml","content":"# Runanywhere_qhexrt Skill\n\nPath: `bindings/flutter/packages/runanywhere_qhexrt/pubspec.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_qhexrt/pubspec.yaml)","tokens":300,"sizeBytes":252},{"name":"CHANGELOG.md","path":"bindings/flutter/packages/runanywhere/CHANGELOG.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere/CHANGELOG.md","title":"Runanywhere Skill","category":"anthropic-skill","format":"markdown","content":"# Runanywhere Skill\n\nPath: `bindings/flutter/packages/runanywhere/CHANGELOG.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere/CHANGELOG.md)","tokens":300,"sizeBytes":231},{"name":"pubspec.yaml","path":"bindings/flutter/packages/runanywhere/pubspec.yaml","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere/pubspec.yaml","title":"Runanywhere Skill","category":"anthropic-skill","format":"yaml","content":"# Runanywhere Skill\n\nPath: `bindings/flutter/packages/runanywhere/pubspec.yaml`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere/pubspec.yaml)","tokens":300,"sizeBytes":231},{"name":"nitro.json","path":"bindings/react-native/packages/core/nitro.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/core/nitro.json","title":"Core Skill","category":"anthropic-skill","format":"json","content":"# Core Skill\n\nPath: `bindings/react-native/packages/core/nitro.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/core/nitro.json)","tokens":300,"sizeBytes":216},{"name":"package.json","path":"bindings/react-native/packages/core/package.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/core/package.json","title":"Core Skill","category":"anthropic-skill","format":"json","content":"# Core Skill\n\nPath: `bindings/react-native/packages/core/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/core/package.json)","tokens":300,"sizeBytes":220},{"name":"tsconfig.json","path":"bindings/react-native/packages/core/tsconfig.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/core/tsconfig.json","title":"Core Skill","category":"anthropic-skill","format":"json","content":"# Core Skill\n\nPath: `bindings/react-native/packages/core/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/core/tsconfig.json)","tokens":300,"sizeBytes":222},{"name":"tsconfig.test.json","path":"bindings/react-native/packages/core/tsconfig.test.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/core/tsconfig.test.json","title":"Core Skill","category":"anthropic-skill","format":"json","content":"# Core Skill\n\nPath: `bindings/react-native/packages/core/tsconfig.test.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/core/tsconfig.test.json)","tokens":300,"sizeBytes":232},{"name":"nitro.json","path":"bindings/react-native/packages/llamacpp/nitro.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/llamacpp/nitro.json","title":"Llamacpp Skill","category":"anthropic-skill","format":"json","content":"# Llamacpp Skill\n\nPath: `bindings/react-native/packages/llamacpp/nitro.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/llamacpp/nitro.json)","tokens":300,"sizeBytes":228},{"name":"package.json","path":"bindings/react-native/packages/llamacpp/package.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/llamacpp/package.json","title":"Llamacpp Skill","category":"anthropic-skill","format":"json","content":"# Llamacpp Skill\n\nPath: `bindings/react-native/packages/llamacpp/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/llamacpp/package.json)","tokens":300,"sizeBytes":232},{"name":"tsconfig.json","path":"bindings/react-native/packages/llamacpp/tsconfig.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/llamacpp/tsconfig.json","title":"Llamacpp Skill","category":"anthropic-skill","format":"json","content":"# Llamacpp Skill\n\nPath: `bindings/react-native/packages/llamacpp/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/llamacpp/tsconfig.json)","tokens":300,"sizeBytes":234},{"name":"package.json","path":"bindings/react-native/packages/mlx/package.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/mlx/package.json","title":"Mlx Skill","category":"anthropic-skill","format":"json","content":"# Mlx Skill\n\nPath: `bindings/react-native/packages/mlx/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/mlx/package.json)","tokens":300,"sizeBytes":217},{"name":"tsconfig.json","path":"bindings/react-native/packages/mlx/tsconfig.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/mlx/tsconfig.json","title":"Mlx Skill","category":"anthropic-skill","format":"json","content":"# Mlx Skill\n\nPath: `bindings/react-native/packages/mlx/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/mlx/tsconfig.json)","tokens":300,"sizeBytes":219},{"name":"nitro.json","path":"bindings/react-native/packages/onnx/nitro.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/onnx/nitro.json","title":"Onnx Skill","category":"anthropic-skill","format":"json","content":"# Onnx Skill\n\nPath: `bindings/react-native/packages/onnx/nitro.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/onnx/nitro.json)","tokens":300,"sizeBytes":216},{"name":"package.json","path":"bindings/react-native/packages/onnx/package.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/onnx/package.json","title":"Onnx Skill","category":"anthropic-skill","format":"json","content":"# Onnx Skill\n\nPath: `bindings/react-native/packages/onnx/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/onnx/package.json)","tokens":300,"sizeBytes":220},{"name":"tsconfig.json","path":"bindings/react-native/packages/onnx/tsconfig.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/onnx/tsconfig.json","title":"Onnx Skill","category":"anthropic-skill","format":"json","content":"# Onnx Skill\n\nPath: `bindings/react-native/packages/onnx/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/onnx/tsconfig.json)","tokens":300,"sizeBytes":222},{"name":"nitro.json","path":"bindings/react-native/packages/qhexrt/nitro.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/qhexrt/nitro.json","title":"Qhexrt Skill","category":"anthropic-skill","format":"json","content":"# Qhexrt Skill\n\nPath: `bindings/react-native/packages/qhexrt/nitro.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/qhexrt/nitro.json)","tokens":300,"sizeBytes":222},{"name":"package.json","path":"bindings/react-native/packages/qhexrt/package.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/qhexrt/package.json","title":"Qhexrt Skill","category":"anthropic-skill","format":"json","content":"# Qhexrt Skill\n\nPath: `bindings/react-native/packages/qhexrt/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/qhexrt/package.json)","tokens":300,"sizeBytes":226},{"name":"tsconfig.json","path":"bindings/react-native/packages/qhexrt/tsconfig.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/qhexrt/tsconfig.json","title":"Qhexrt Skill","category":"anthropic-skill","format":"json","content":"# Qhexrt Skill\n\nPath: `bindings/react-native/packages/qhexrt/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/qhexrt/tsconfig.json)","tokens":300,"sizeBytes":228},{"name":"tsconfig.test.json","path":"bindings/react-native/packages/qhexrt/tsconfig.test.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/qhexrt/tsconfig.test.json","title":"Qhexrt Skill","category":"anthropic-skill","format":"json","content":"# Qhexrt Skill\n\nPath: `bindings/react-native/packages/qhexrt/tsconfig.test.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/qhexrt/tsconfig.test.json)","tokens":300,"sizeBytes":238},{"name":"package.json","path":"bindings/web/packages/core/package.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/core/package.json","title":"Core Skill","category":"anthropic-skill","format":"json","content":"# Core Skill\n\nPath: `bindings/web/packages/core/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/core/package.json)","tokens":300,"sizeBytes":202},{"name":"tsconfig.json","path":"bindings/web/packages/core/tsconfig.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/core/tsconfig.json","title":"Core Skill","category":"anthropic-skill","format":"json","content":"# Core Skill\n\nPath: `bindings/web/packages/core/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/core/tsconfig.json)","tokens":300,"sizeBytes":204},{"name":"tsconfig.test.json","path":"bindings/web/packages/core/tsconfig.test.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/core/tsconfig.test.json","title":"Core Skill","category":"anthropic-skill","format":"json","content":"# Core Skill\n\nPath: `bindings/web/packages/core/tsconfig.test.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/core/tsconfig.test.json)","tokens":300,"sizeBytes":214},{"name":"package.json","path":"bindings/web/packages/llamacpp/package.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/llamacpp/package.json","title":"Llamacpp Skill","category":"anthropic-skill","format":"json","content":"# Llamacpp Skill\n\nPath: `bindings/web/packages/llamacpp/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/llamacpp/package.json)","tokens":300,"sizeBytes":214},{"name":"tsconfig.json","path":"bindings/web/packages/llamacpp/tsconfig.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/llamacpp/tsconfig.json","title":"Llamacpp Skill","category":"anthropic-skill","format":"json","content":"# Llamacpp Skill\n\nPath: `bindings/web/packages/llamacpp/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/llamacpp/tsconfig.json)","tokens":300,"sizeBytes":216},{"name":"package.json","path":"bindings/web/packages/onnx/package.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/onnx/package.json","title":"Onnx Skill","category":"anthropic-skill","format":"json","content":"# Onnx Skill\n\nPath: `bindings/web/packages/onnx/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/onnx/package.json)","tokens":300,"sizeBytes":202},{"name":"tsconfig.json","path":"bindings/web/packages/onnx/tsconfig.json","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/onnx/tsconfig.json","title":"Onnx Skill","category":"anthropic-skill","format":"json","content":"# Onnx Skill\n\nPath: `bindings/web/packages/onnx/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/onnx/tsconfig.json)","tokens":300,"sizeBytes":204},{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# AGENTS.md\n\nGuidance for AI coding assistants working in this repository.\n\n> **`AGENTS.md` is the real file; each `CLAUDE.md` is a committed symlink to the\n> `AGENTS.md` beside it.** Editing either name edits the same bytes. If you create a new\n> `AGENTS.md` in a directory that doesn't have one, run\n> `bash scripts/validation/gates/check_agents_claude_sync.sh --fix` from the repo root to\n> create its `CLAUDE.md` symlink — `pr-build.yml` fails any tracked `AGENTS.md` missing one.\n> Never hand-create a `CLAUDE.md` as a real file.\n\n## Working conventions\n\n- **Focus on simplicity.** Clean, SOLID-ish separation of concerns; reuse over\n  reinvention. No mock implementations and no new unit tests unless the task explicitly\n  asks for them.\n- **Plan first for non-trivial work.** Write the plan to\n  `thoughts/shared/plans/{descriptive_name}.md`, get it approved before implementing, and\n  keep it updated (append what changed) as you go so the next engineer can pick it up.\n- **Structured types over raw strings.** Enums/sealed classes/data classes for\n  configuration and results — this is enforced per-SDK by lint (ktlint/detekt, SwiftLint,\n  ESLint) so ad hoc strings tend to get caught anyway.\n- **Read files fully** before editing them; only use offset/limit on files too large to\n  read at once.\n- **Use full local build parallelism** (`-j \"$(sysctl -n hw.logicalcpu)\"` / equivalent),\n  scaling down only under real memory/thermal pressure — not because load average looks\n  high.\n- **Multi-agent workflows run on Sonnet.** When fanning work out to subagents (the\n  `Workflow` tool, or several parallel `Agent` calls), pass `model: 'sonnet'` on every\n  `agent(...)` call rather than letting them inherit the session model. A fan-out of\n  8-10 probes reading large files is exactly the shape where Sonnet is the right\n  cost/latency trade, and these agents are doing bounded, well-specified retrieval and\n  analysis rather than open-ended reasoning. Reserve the session's stronger model for\n  the synthesis step that consumes their reports, and for anything where a wrong answer\n  is expensive and hard to detect — a final design decision, an adversarial verification\n  pass, or a security-relevant judgement.\n\n  ```js\n  // in a Workflow script\n  const findings = await parallel(LENSES.map((l) => () =>\n    agent(l.prompt, { label: `probe:${l.key}`, schema: FINDINGS, model: 'sonnet' })))\n\n  // the synthesis that reads them can stay on the session model\n  const plan = await agent(synthesisPrompt, { schema: PLAN, effort: 'high' })\n  ```\n\n## The most important architectural rule: logic lives at the lowest layer that serves everyone\n\nEach feature/modality (LLM, STT, TTS, VAD, VLM, RAG, LoRA, Voice) is invoked through\n**one** SDK entry point; the SDK, and below it C++ commons, does all the heavy lifting. If\nan example app builds a multi-step bootstrap sequence, hardcodes a model/engine constant,\nor post-processes model output, that is a bug in the SDK, not the app — fix it down a\nlayer.\n\n1. **C++ commons (`core/`)** — cross-platform, non-I/O logic: model lifecycle, registry,\n   download orchestration, RAG session management, inference routing. All 5 SDKs get the\n   fix for free.\n2. **Platform SDK layer** — platform-specific I/O or runtime bridging only (Web OPFS,\n   iOS Keychain, Android Keystore, WASM MEMFS mirroring).\n3. **Example apps** — UI rendering, navigation, thin SDK calls. No business logic, no\n   internal-SDK knowledge (path patterns, framework→directory mappings), no workarounds.\n\n**iOS is the source of truth.** When behavior is ambiguous in any other SDK, check the\niOS Swift implementation first and copy the logic exactly, adapting only syntax.\n\n---\n\n## Repository overview\n\nCross-platform on-device AI SDK monorepo. A single C/C++ core (`runanywhere-commons`,\n~118K first-party LOC plus ~420K generated proto bindings) implements all AI business\nlogic behind a pure C ABI (`rac_*` prefix). Platform SDKs are thin bridges that supply\nplatform services (file I/O, HTTP, Keychain, audio) via an inversion-of-control struct and\ncall into the C core for all inference. Protobuf IDL schemas generate type-safe bindings\nfor every language.\n\n**Current version**: `0.20.37` (canonical source: `core/VERSION`)\n\n| SDK | Path | Bridge mechanism | Platforms |\n|-----|------|-------------------|-----------|\n| Swift | `bindings/swift/` | XCFramework + CRACommons module map | iOS 17.5+, macOS 14.5+ |\n| Kotlin | `bindings/kotlin/` | JNI (`librunanywhere_jni.so`) | Android (min API 24) |\n| Flutter | `bindings/flutter/` | Dart FFI | iOS, Android |\n| React Native | `bindings/react-native/` | NitroModules (JSI HybridObject) | iOS 17.5+, Android arm64 |\n| Web | `bindings/web/` | Emscripten WASM + TypeScript | Chrome, Safari, Firefox |\n| Electron | `bindings/electron/` | Node-API addon | Windows x64 (preview) |\n| Python | `bindings/python/` | pybind11 extension (`runanywhere._core`) | Windows, macOS, Linux (alpha) |\n\nEach has its own `AGENTS.md` with full architecture, build commands, and conventions —\nread it before working in that SDK.\n\n| Native dir | Contents |\n|-----------|----------|\n| `core/` | C/C++ core library: all AI logic, plugin registry, event system |\n| `engines/` | 7 backend plugins: llamacpp, sherpa, onnx, cloud, mlx, qhexrt, neurt |\n| `runtimes/` | 3 runtime adapters: cpu (always), onnxrt, coreml |\n| `idl/` | 39 Protobuf schemas + per-language codegen scripts |\n\n### Consumer apps live in separate repos\n\nThe four full consumer apps were extracted into standalone repos (history preserved); PR\nagainst them there, not here: [runanywhere-ios](https://github.com/RunanywhereAI/runanywhere-ios),\n[runanywhere-android](https://github.com/RunanywhereAI/runanywhere-android),\n[runanywhere-web](https://github.com/RunanywhereAI/runanywhere-web),\n[runanywhere-electron](https://github.com/RunanywhereAI/runanywhere-electron).\n\nTwo example apps remain in-tree (`bindings/flutter/example/`, `bindings/react-native/example/`).\nAll example apps share brand orange `#FF6900`, documented in `docs/DESIGN_GUIDELINE.md`.\n\n### Minimal examples are how you verify an SDK change locally\n\nEach in-repo example consumes its SDK from local source (no staging/publishing needed to\nsee an edit): Swift via `RUNANYWHERE_USE_LOCAL_NATIVES=1`, Kotlin via a Gradle composite\nbuild (`includeBuild` + `dependencySubstitution`), Web via Vite aliases into\n`packages/*/src`. They're deliberately minimal (one prompt in, one streamed completion\nout) — feature-complete UI belongs in the consumer repos above.\n\n---\n\n## Cross-platform architecture\n\n`idl/*.proto` is the schema root; `idl/codegen/generate_all.sh` emits per-language\nbindings (Swift, Kotlin/Wire, TS/ts-proto, Dart), all generated and none committed — see\n[`docs/reference/generated-code-contract.md`](docs/reference/generated-code-contract.md)\nfor the full \"what generates what, when\" contract before touching `idl/`.\n\nAll SDKs reach `runanywhere-commons` through the `rac_*` C API. Commons holds the\ncomponent layer (lifecycle), the service layer (dispatch), and the plugin registry, and\nreaches engines through `rac_engine_vtable_t`.\n\n| Engine | Primitives |\n|---|---|\n| llamacpp | LLM, VLM |\n| sherpa-onnx | STT, TTS, VAD |\n| onnx | Embed, Segment |\n| qhexrt | Hexagon NPU |\n| neurt, cloud | Apple Neural Engine, HTTP |\n\nBackend base priorities (highest wins per primitive via `rac_plugin_find()`, no\nruntime/format scoring): qhexrt=150 (QNN-context models only), mlx=110 (Apple),\nllamacpp=100, sherpa=90, onnx/cloud=50. An explicit engine name is honored through\n`rac_plugin_find_for_engine()` regardless of priority.\n\n### Non-obvious invariants that span every SDK\n\n- **Platform adapter IoC**: `rac_platform_adapter_t` is a flat C struct of function\n  pointers populated by each SDK before `rac_init()`. C++ never calls platform APIs\n  directly — file I/O, HTTP, Keychain, logging, memory queries all pass through it.\n- **Two-phase init**: every SDK does Phase 1 (sync: register adapter, load native libs,\n  configure logging) then Phase 2 (async: authenticate, register device, fetch model\n  assignments, discover downloaded models).\n- **Plugin ABI**: `RAC_PLUGIN_API_VERSION = 9u`; a version mismatch is an immediate\n  rejection. Each vtable has 10 active primitive slots and 7 reserved. NULL slot = not\n  supported. `rerank_ops` (wire value 11) was promoted from `reserved_slot_2` in ABI v8;\n  wire value 6, retired in ABI v4, stays permanently retired — never reuse it. See\n  `core/AGENTS.md` for the full ABI history and how to add a primitive.\n- **Static vs. dynamic plugins**: iOS and WASM force `RAC_STATIC_PLUGINS=ON` (no\n  `dlopen`), registered via `RAC_STATIC_PLUGIN_REGISTER(name)` + `-force_load` /\n  `--whole-archive`. Android/Linux/macOS default to dynamic loading via\n  `rac_registry_load_plugin()`.\n- **Streaming fan-out**: C++ allows only one proto-byte callback per component handle.\n  Each SDK implements its own fan-out to multiplex that into multiple subscribers (Swift\n  `AsyncStream`, Kotlin `Flow`, Dart `StreamController`, TS `AsyncIterable`).\n- **Proto types are canonical**: never hand-write enum values or structured types that\n  exist in `idl/*.proto` — use the generated types/typealiases and regenerate instead.\n- **HTTP is platform-provided**: no libcurl. Each SDK registers a\n  `rac_http_transport_ops_t` (URLSession on Apple platforms, OkHttp on Android,\n  `emscripten_fetch` on Web).\n- **NeuRT crosses a repo boundary as BYTES, not source**: the Apple engine is built and\n  published by the private `neurun` repo and consumed here as prebuilt archives, pinned by\n  tag + SHA-256 in `core/VERSIONS` (`NEURT_*`). Fetch with `scripts/build/download-neurt.sh`.\n  Each archive carries a receipt naming the `RAC_PLUGIN_API_VERSION` it was built against,\n  and both the downloader and CMake refuse a mismatch — a vtable-layout change relinks\n  cleanly and corrupts dispatch at runtime, so nothing else would catch it.\n\nA side-by-side comparison of entry point / bridge / streaming / events / storage / HTTP\nper SDK — useful when porting a fix across SDKs — lives in\n[`docs/reference/cross-sdk-parity.md`](docs/reference/cross-sdk-parity.md).\n\n---\n\n## Building and running\n\nThe root `CMakeLists.txt` (version from `core/VERSION`) is the single entry point for\nnative builds; `CMakePresets.json` defines `macos-{debug,release}`, `linux-{debug,release,asan}`,\n`ios-{device,simulator}`, `android-arm64`, `wasm`, `cpp-desktop-*`, and the `windows-*` presets.\n\n```bash\ncmake --preset macos-debug && cmake --build build/macos-debug && ctest --preset macos-debug\n```\n\n**`./run <group> [subcommand]`** is the unified wrapper for everything else — build,\nlint, and run any SDK or example app from one CLI (`./run --help` for the full menu):\n\n```bash\n./run doctor                    # scan host toolchains, show what's buildable here\n./run setup                     # provision env for every host-buildable target\n./run sdk commons build-android # build C++ commons for all Android ABIs, stage .so files\n./run sdk kotlin build          # (etc. — see per-SDK AGENTS.md for direct tool commands)\n./run example android install   # build + install + launch the Kotlin minimal example\n./run codegen                   # regenerate every language binding from idl/\n```\n\nFor the direct `swift build` / `./gradlew` / `melos` / `yarn` / `npm` commands each SDK\nuses under the hood, and its test/lint/publish targets, see that SDK's own `AGENTS.md`\n(`bindings/<name>/AGENTS.md`) — the commands are kept there, not duplicated here, so they\ncan't drift.\n\nCross-platform build scripts worth knowing about at the root level:\n\n```bash\n./bindings/swift/scripts/build-core-xcframework.sh   # also syncs into RN/Flutter plugin dirs\n./scripts/build/build-core-android.sh                # .so for all ABIs → every SDK's jniLibs/\n./bindings/web/scripts/build-core-wasm.sh\n./scripts/setup/setup-toolchain.sh                   # protoc/wire/ts-proto toolchain\n./idl/codegen/generate_all.sh [--only <lang>]         # swift|kotlin|dart|ts|cpp|python\n```\n\n---\n\n## Version management and releases\n\nCanonical version: `core/VERSION` (single-line semver). Bump everywhere with:\n\n```bash\n./scripts/release/sync-versions.sh <version>\n```\n\nThe full release runbook (version bump → PR → tag → GitHub Release → npm/Maven/pub.dev/\nSwiftPM publishing → cutting the `runanywhere-swift` SPM distribution repo → starter-app\nsmoke tests) is a multi-step process captured in three skills — use them rather than\nimprovising the steps: **sdk-release** (version bump through published GitHub Release),\n**sdk-publish** (registry publishing + the `runanywhere-swift` dist repo cut), and\n**sdk-test-starters** (post-release device/app smoke tests).\n\n### Release pipeline lessons (v0.20.29 through v0.20.33 — read before cutting a release by hand)\n\n- **`native_ios` and `native_android` always rebuild fresh archives on every `release.yml`\n  run — there is no reuse/skip mechanism for either job** (unlike `native_web`'s\n  `reuse_native_web_run_id` or the cpp-desktop kits' `reuse_cpp_desktop_run_id`). This makes\n  the Apple/Android TEMP-pin convention (Package.swift's `sdkVersion`, the Flutter podspecs'\n  `asset_version`, Kotlin's `nativeLibVersion` pinned to an old version \"because this release\n  ships no new iOS/Android archives\") **structurally unsound for any release where those two\n  jobs actually run — which is every release.** It surfaced as three separate bugs\n  (`auto-tag.yml`'s coherence check, `package-sdk.sh`'s podspec check, and `sync-checksums.sh`'s\n  publish-time verification) before the pattern was recognized. If a release genuinely ships\n  no new Apple/Android bytes, that has to be proven by diffing the actual built artifacts\n  against the previous release, not assumed from what changed in the diff.\n- **`RunAnywhereMLXRuntime`'s stripped Mach-O is not byte-reproducible across builds**\n  (`RAC_CHECKSUM_SKIP=RunAnywhereMLXRuntime` in `release.yml`'s publish job exists because of\n  this, and is the ONLY archive exempted from the publish-time checksum gate). The corollary\n  that bit v0.20.29: **the checksum committed to `Package.swift` / the Flutter MLX podspec\n  can only be computed from a build that already happened, but the tag that freezes those\n  files has to be pushed before `native_ios` (which produces that exact artifact) even runs**\n  — so re-syncing checksums against an EARLIER, superseded build (e.g. after a tag had to be\n  moved and re-dispatched to fix an unrelated bug) commits a value that does not match what\n  the FINAL successful run actually publishes, and the skip means nothing catches it. v0.20.29\n  shipped this way (committed `d942788f...`, actually published `fc4b23a0...`) — caught only\n  because the Flutter `runanywhere_mlx` package hadn't been published to pub.dev yet for that\n  version. Before publishing any package that ships `RunAnywhereMLXRuntime`, re-run\n  `bindings/swift/scripts/sync-checksums.sh` against the artifacts from the SPECIFIC run whose\n  tag is being published — never reuse a checksum computed against an earlier attempt.\n- **The Electron QHexRT backend never set `ADSP_LIBRARY_PATH`, and one comment in the\n  codebase confidently asserted Windows doesn't need it.** It does, for exactly one decode\n  path: `QHexRT/src/bonsai/fastrpc_win.cpp`'s custom FastRPC skel (the ternary/Bonsai\n  decoder, e.g. `qwen3.8-27b-1bit-npu`) resolves through `ADSP_LIBRARY_PATH ∪ cwd`, separately\n  from the standard QNN HTP graph path's `LoadLibraryW` + PATH search that `bridge.ts`'s\n  `addSidecarDirToDllSearch` already handled. Every OTHER QHexRT model worked fine in the\n  packaged app, which is exactly what let this ship undetected — validate the Bonsai/ternary\n  path specifically, not just \"some NPU model works.\" Fixed in `bridge.ts`'s\n  `addSidecarDirToDspSearchPath` (#803).\n- **`package-private-engine-overlay.sh`'s QAIRT-runtime copy step filtered on `{\".dll\",\n  \".lib\"}` only**, silently dropping every `.so`/`.cat` skel file (both the standard HTP skel\n  and, since it never read `engines/qhexrt/prebuilt/current/dsp/win-arm64/` at all, the\n  Bonsai skel too). This meant **RCLI could not have run any QHexRT model** — standard or\n  ternary — from a fresh private overlay, a gap invisible until someone actually tried to run\n  inference rather than just building. `bindings/electron/scripts/bundle-native.ts` already\n  had the correct pattern (copy `dsp/win-arm64/` flat, no extension filter); the overlay\n  script just never matched it. Fixed in #804.\n- **A new NeuRT archive must be added in FOUR places, and three of them only fail in a build\n  configuration your Mac never produces.** v0.20.33 added `libneurt_rac_tts_ops.a` and took two\n  wasted release candidates to land, because each site fails somewhere different and late:\n  `engines/neurt/CMakeLists.txt` (probe list + imported target + `_neurt_extra_links`),\n  `build-core-xcframework.sh` (pre-flight guard AND `_NEURT_ROUTABLE_ARCHIVES` — two lists in one\n  file), `package-private-engine-overlay.sh`, and the `#if RAC_NEURT_ROUTABLE` guard around the\n  vtable entry in `rac_plugin_entry_neurt.cpp`. Miss the force-load lists and every native job goes\n  green while `validate_consumer_flutter` dies an hour later on `Undefined symbol:\n  _g_neurt_tts_ops` — the engine registers, all CMake targets compile, and only a CONSUMER's link\n  fails. Miss the vtable guard and the routable build is fine while every PR job WITHOUT\n  `NEURUN_TOKEN` fails, because a non-routable shell still references a symbol nothing defines.\n  A dev machine with the payload staged is routable, so it reproduces neither. `libneurt_rac_*.a`\n  archives now fail packaging by name if they are not in the list\n  (`neurt_assert_no_unlisted_archives`); the vtable guard has no such mechanism — audit all ten\n  slots when adding one. Reproduce a non-routable build by moving `core/third_party/neurt` aside.\n- **`reuse_native_web_run_id` is SAME-VERSION-ONLY, whatever the diff looks like.** `racommons.wasm`\n  embeds `RAC_VERSION_STRING` and `validate_public_packages.py` asserts it against the version being\n  packaged, so reusing ANY prior release's run fails `sdk_web` with *\"does not embed\n  RAC_VERSION_STRING\"*. Worse than a red job: `native_web` is skipped, so that candidate never\n  uploads a web artifact and can never serve as `publish_from_run_id`. Only a re-dispatch at the\n  same version qualifies.\n- **Verify checksums by NAME as well as by URL — the manifests use two different shapes.** The root\n  `Package.swift` interpolates `\\(sdkVersion)` into each URL; the Flutter manifests bind a checksum\n  to a `name:` through a helper with no URL at the call site at all. A URL-only cross-check silently\n  validates 9 of 15 sites and reports success. Re-hash every site against the zip it names, and\n  remember `RAC_CHECKSUM_SKIP` hides `RunAnywhereMLXRuntime` from `publish` entirely, so the Flutter\n  MLX podspec is verifiable only by hand.\n- **A green `rcli backends --json` proves nothing against the OSS kit.** NeuRT ships as a separate\n  private pack, so the public bottle lists no `neurt` at all — a routability check there passes\n  cleanly and means nothing. Apply the private overlay\n  (`cpp-desktop-macos-arm64-neurt-private` workflow artifact) before believing the result.\n- **General pattern across all four of the above**: each one was invisible to the checks that\n  existed (a version-coherence check, a checksum-skip escape hatch, a \"the model loaded\"\n  smoke test, a \"the build succeeded\" CI job) because none of those checks actually ran the\n  specific thing that was broken. When validating a release, run the SPECIFIC flagship/hardest\n  model end-to-end on-device, not just confirm the pipeline completed.\n- **The RACommons/RunAnywhereMLXRuntime checksum-staleness bug above recurred on v0.20.30 AND\n  v0.20.31** — same `mismatch: RACommonsBinary` failure in `publish`, same root cause both\n  times: the version-bump PR ran `sync-versions.sh` and merged/tagged directly, skipping the\n  **mandatory** \"dispatch a release candidate off the unmerged branch, sync checksums from\n  its real build, THEN merge\" sequence that the **`sdk-release` skill's step 2-3 already\n  document in detail** (including this exact failure signature). Documenting the lesson in\n  prose here was not enough to stop it recurring twice more — **if you are about to run\n  `scripts/release/sync-versions.sh` for ANY reason, including as one step of a larger\n  cross-repo release cascade, invoke the `sdk-release` skill and follow it from step 1**,\n  rather than reconstructing the sequence from memory or treating a \"small\" version bump as\n  exempt. v0.20.31 additionally caught `RunAnywhereMLXRuntime` in the Flutter podspec\n  independently stale in the SAME commit — `RAC_CHECKSUM_SKIP` exempts it from `publish`'s\n  check, so it would have shipped broken to Flutter consumers with no CI signal at all; check\n  every row of the checksum-location table, not only the one the CI error names.\n- **QHexRT's `qhx_runtime_device()` (arch/soc lookup, needed to pick the `v75`/`v79`/`v81`\n  manifest directory in `session_open()`) shared its live-device cost with the standard QNN\n  graph path**, so a `host_only` model with ZERO QNN graphs — the Bonsai/Maple ternary decoder\n  (`qwen3.8-27b-1bit-npu`) — paid for a live QNN HTP device anyway, which then contended with\n  the decoder's own direct FastRPC session for the same Hexagon cDSP and made every generation\n  fail with a bare `HostOpFailed`. This shipped in v0.20.30 (the SAME release that fixed the\n  `ADSP_LIBRARY_PATH` bug for the same model) and was found only by running that SPECIFIC model\n  on real Windows-ARM64 hardware after the \"fix\" — reinforcing the point above: a green\n  `HostOpFailed`-free run of a DIFFERENT QHexRT model proves nothing about this one. Two red\n  herrings were chased and ruled out with live device tracing before the real cause: neither\n  `ADSP_LIBRARY_PATH` (confirmed correctly set) nor a QAIRT-version mismatch between the\n  shipped host DLLs and the on-device skel (confirmed present, but fixing it alone did not fix\n  the failure) was the actual cause. Fixed in `neurun` v0.20.31 (`Backend::profile()` now uses\n  a separate, device-handle-free `deviceGetPlatformInfo` query instead of sharing\n  `ensure_device()` with `device()`) + this repo's `qhexrt_session.cpp` (#810, a complementary\n  fix removing a second, redundant eager device query used only for a log line).\n\n---\n\n## CI/CD workflows (`.github/workflows/`)\n\n| Workflow | Trigger | Purpose |\n|----------|---------|---------|\n| `pr-build.yml` | PR to main, push to main/feat branch | Parallel native builds (macOS/Linux/iOS/Android) + per-SDK typecheck + centralization/coherence gates |\n| `release.yml` | Tag `v*.*.*` or manual | Full artifact build matrix, SDK packaging, consumer validation, draft Release |\n| `auto-tag.yml` | PR merged to main with `release:*` label | Verifies the reviewed semver bump, pushes that exact git tag |\n| `idl-drift-check.yml` | Changes to `idl/` or generated files | Regenerates protos, verifies schema lock + untracked generated trees |\n| `legacy-files-blocklist.yml` | All PRs/pushes | Prevents specific deleted files from being re-introduced |\n| `secret-scan.yml` | PRs and pushes to main | Incremental gitleaks scan on diff range |\n| `check-no-pii-logging.yml` | PRs/pushes to main/master/feat-branch | Guards against logging signed URLs alongside active-download destination paths |\n\n---\n\n## Platform requirements\n\n| Platform | Min version | Build tool | Key versions |\n|----------|------------|------------|--------------|\n| iOS / macOS | 17.5 / 14.5 | Xcode 26+ | Swift 6.2 |\n| Kotlin SDK | Android API 24 | AGP 9.2.1 / Gradle 9.5.0 | Kotlin 2.4.0, NDK 27.3.13750724 |\n| Flutter | 3.44.6 | Melos / AGP 9.0.1 / Gradle 9.1.0 | Dart 3.12.2+, NDK 28.2.13676358 |\n| React Native | 0.85.3 (min 0.83.1) | Yarn Berry 3.6.1 | NitroModules, Hermes |\n| Web | Chrome 86+ | Vite | Emscripten 6.0.2, Node 24 LTS |\n| C++ core | N/A | CMake 3.24+ | C++20, Ninja |\n\n---\n\n## Non-obvious configuration details\n\n- **NDK pin**: `core/VERSIONS::NDK_VERSION` (27.3.13750724) is the single source of truth,\n  mirrored into `bindings/kotlin/gradle.properties`. NDK 27 (r27d) gives 16 KB\n  page-alignment required by Android 15+ — NDK 25.x's 4 KB-aligned `libc++_shared.so` /\n  `libomp.so` trips Android 16's page-size enforcement. Mirror this pin whenever bumping\n  it; Flutter/RN Android build files carry their own fallback literals that can drift.\n- **`useLocalNatives`** (Kotlin `gradle.properties`, similarly named flags elsewhere):\n  `true` builds native libs from source locally; CI and most non-local runs set it\n  `false` to download prebuilt `.so`/`.xcframework`/`.wasm` from GitHub Releases instead.\n- **Web SDK** has several hard-won WASM/browser runtime workarounds (VLM worker crash\n  recovery, a Qwen2-VL WebGPU NaN bug, cross-origin isolation requirements) — see\n  [`docs/reference/web-runtime-gotchas.md`](docs/reference/web-runtime-gotchas.md).\n- **Cloud/Linux dev VM** (e.g. Cursor Cloud): what's buildable there and the\n  clang/NDK/KVM gotchas are in\n  [`docs/reference/cursor-cloud-environment.md`](docs/reference/cursor-cloud-environment.md).\n\n---\n\n## Pre-commit hooks\n\n```bash\npre-commit run --all-files                     # everything\npre-commit run ios-sdk-swiftlint --all-files   # SwiftLint only\n```\n\nConfigured hooks: gitleaks (secrets), trailing-whitespace, end-of-file-fixer, check-yaml,\ncheck-added-large-files (1000 KB max), check-merge-conflict, object file detection,\nSwiftLint (SDK + example app), periphery (unused Swift code detection).\n\n---\n\n## Where to look for current in-flight work\n\n`thoughts/shared/plans/` holds active/completed execution plans (including any in-flight\nSDK reorg or migration briefs — e.g. the Electron reorganization plan); `thoughts/shared/issues/`\nholds tracked bug write-ups. Check these directories directly rather than trusting a\nstatus summary written into this file — both change too often for a static snapshot here\nto stay accurate.\n","isInternal":false,"tokens":6705,"sizeBytes":26163},{"name":"CLAUDE.md","path":"core/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/core/CLAUDE.md","title":"core - Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# core - Claude Agent Guidelines & System Prompt\n\nPath: `core/CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/core/CLAUDE.md)","tokens":300,"sizeBytes":188},{"name":"CLAUDE.md","path":"engines/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/engines/CLAUDE.md","title":"engines - Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# engines - Claude Agent Guidelines & System Prompt\n\nPath: `engines/CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/engines/CLAUDE.md)","tokens":300,"sizeBytes":197},{"name":"CLAUDE.md","path":"idl/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/idl/CLAUDE.md","title":"idl - Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# idl - Claude Agent Guidelines & System Prompt\n\nPath: `idl/CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/idl/CLAUDE.md)","tokens":300,"sizeBytes":185},{"name":"CLAUDE.md","path":"runtimes/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/runtimes/CLAUDE.md","title":"runtimes - Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# runtimes - Claude Agent Guidelines & System Prompt\n\nPath: `runtimes/CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/runtimes/CLAUDE.md)","tokens":300,"sizeBytes":200},{"name":"CLAUDE.md","path":"scripts/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/scripts/CLAUDE.md","title":"scripts - Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# scripts - Claude Agent Guidelines & System Prompt\n\nPath: `scripts/CLAUDE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/scripts/CLAUDE.md)","tokens":300,"sizeBytes":197},{"name":"README.md","path":"bindings/electron/packages/llamacpp/README.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/llamacpp/README.md","title":"llamacpp Documentation","category":"plugin-manifest","format":"markdown","content":"# llamacpp Documentation\n\nPath: `bindings/electron/packages/llamacpp/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/llamacpp/README.md)","tokens":300,"sizeBytes":226},{"name":"README.md","path":"bindings/electron/packages/neurt/README.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/neurt/README.md","title":"neurt Documentation","category":"plugin-manifest","format":"markdown","content":"# neurt Documentation\n\nPath: `bindings/electron/packages/neurt/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/neurt/README.md)","tokens":300,"sizeBytes":217},{"name":"README.md","path":"bindings/electron/packages/onnx/README.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/onnx/README.md","title":"onnx Documentation","category":"plugin-manifest","format":"markdown","content":"# onnx Documentation\n\nPath: `bindings/electron/packages/onnx/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/onnx/README.md)","tokens":300,"sizeBytes":214},{"name":"README.md","path":"bindings/electron/packages/qhexrt/README.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/qhexrt/README.md","title":"qhexrt Documentation","category":"plugin-manifest","format":"markdown","content":"# qhexrt Documentation\n\nPath: `bindings/electron/packages/qhexrt/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/qhexrt/README.md)","tokens":300,"sizeBytes":220},{"name":"README.md","path":"bindings/electron/packages/sherpa/README.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/sherpa/README.md","title":"sherpa Documentation","category":"plugin-manifest","format":"markdown","content":"# sherpa Documentation\n\nPath: `bindings/electron/packages/sherpa/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/sherpa/README.md)","tokens":300,"sizeBytes":220},{"name":"README.md","path":"bindings/flutter/packages/runanywhere_llamacpp/README.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_llamacpp/README.md","title":"runanywhere_llamacpp Documentation","category":"plugin-manifest","format":"markdown","content":"# runanywhere_llamacpp Documentation\n\nPath: `bindings/flutter/packages/runanywhere_llamacpp/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_llamacpp/README.md)","tokens":300,"sizeBytes":260},{"name":"README.md","path":"bindings/flutter/packages/runanywhere_mlx/README.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_mlx/README.md","title":"runanywhere_mlx Documentation","category":"plugin-manifest","format":"markdown","content":"# runanywhere_mlx Documentation\n\nPath: `bindings/flutter/packages/runanywhere_mlx/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_mlx/README.md)","tokens":300,"sizeBytes":245},{"name":"README.md","path":"bindings/flutter/packages/runanywhere_onnx/README.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_onnx/README.md","title":"runanywhere_onnx Documentation","category":"plugin-manifest","format":"markdown","content":"# runanywhere_onnx Documentation\n\nPath: `bindings/flutter/packages/runanywhere_onnx/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_onnx/README.md)","tokens":300,"sizeBytes":248},{"name":"README.md","path":"bindings/flutter/packages/runanywhere_qhexrt/README.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_qhexrt/README.md","title":"runanywhere_qhexrt Documentation","category":"plugin-manifest","format":"markdown","content":"# runanywhere_qhexrt Documentation\n\nPath: `bindings/flutter/packages/runanywhere_qhexrt/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere_qhexrt/README.md)","tokens":300,"sizeBytes":254},{"name":"README.md","path":"bindings/flutter/packages/runanywhere/README.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere/README.md","title":"runanywhere Documentation","category":"plugin-manifest","format":"markdown","content":"# runanywhere Documentation\n\nPath: `bindings/flutter/packages/runanywhere/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/flutter/packages/runanywhere/README.md)","tokens":300,"sizeBytes":233},{"name":"README.md","path":"bindings/react-native/packages/core/README.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/core/README.md","title":"core Documentation","category":"plugin-manifest","format":"markdown","content":"# core Documentation\n\nPath: `bindings/react-native/packages/core/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/core/README.md)","tokens":300,"sizeBytes":222},{"name":"README.md","path":"bindings/react-native/packages/llamacpp/README.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/llamacpp/README.md","title":"llamacpp Documentation","category":"plugin-manifest","format":"markdown","content":"# llamacpp Documentation\n\nPath: `bindings/react-native/packages/llamacpp/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/llamacpp/README.md)","tokens":300,"sizeBytes":234},{"name":"README.md","path":"bindings/react-native/packages/mlx/README.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/mlx/README.md","title":"mlx Documentation","category":"plugin-manifest","format":"markdown","content":"# mlx Documentation\n\nPath: `bindings/react-native/packages/mlx/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/mlx/README.md)","tokens":300,"sizeBytes":219},{"name":"README.md","path":"bindings/react-native/packages/onnx/README.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/onnx/README.md","title":"onnx Documentation","category":"plugin-manifest","format":"markdown","content":"# onnx Documentation\n\nPath: `bindings/react-native/packages/onnx/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/onnx/README.md)","tokens":300,"sizeBytes":222},{"name":"README.md","path":"bindings/react-native/packages/qhexrt/README.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/qhexrt/README.md","title":"qhexrt Documentation","category":"plugin-manifest","format":"markdown","content":"# qhexrt Documentation\n\nPath: `bindings/react-native/packages/qhexrt/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/react-native/packages/qhexrt/README.md)","tokens":300,"sizeBytes":228},{"name":"README.md","path":"bindings/web/packages/core/README.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/core/README.md","title":"core Documentation","category":"plugin-manifest","format":"markdown","content":"# core Documentation\n\nPath: `bindings/web/packages/core/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/core/README.md)","tokens":300,"sizeBytes":204},{"name":"README.md","path":"bindings/web/packages/llamacpp/README.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/llamacpp/README.md","title":"llamacpp Documentation","category":"plugin-manifest","format":"markdown","content":"# llamacpp Documentation\n\nPath: `bindings/web/packages/llamacpp/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/llamacpp/README.md)","tokens":300,"sizeBytes":216},{"name":"README.md","path":"bindings/web/packages/onnx/README.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/onnx/README.md","title":"onnx Documentation","category":"plugin-manifest","format":"markdown","content":"# onnx Documentation\n\nPath: `bindings/web/packages/onnx/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/web/packages/onnx/README.md)","tokens":300,"sizeBytes":204},{"name":"address_PR_comments.md","path":".claude/commands/address_PR_comments.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/.claude/commands/address_PR_comments.md","title":"Command: /address_pr_comments","category":"command-prompt","format":"markdown","content":"# Command: /address_pr_comments\n\nPath: `.claude/commands/address_PR_comments.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/.claude/commands/address_PR_comments.md)","tokens":300,"sizeBytes":221},{"name":"copy_over_logic.md","path":".claude/commands/copy_over_logic.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/.claude/commands/copy_over_logic.md","title":"Command: /copy_over_logic","category":"command-prompt","format":"markdown","content":"# Command: /copy_over_logic\n\nPath: `.claude/commands/copy_over_logic.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/.claude/commands/copy_over_logic.md)","tokens":300,"sizeBytes":209},{"name":"run_and_fix_lints.md","path":".claude/commands/run_and_fix_lints.md","rawUrl":"https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/.claude/commands/run_and_fix_lints.md","title":"Command: /run_and_fix_lints","category":"command-prompt","format":"markdown","content":"# Command: /run_and_fix_lints\n\nPath: `.claude/commands/run_and_fix_lints.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/.claude/commands/run_and_fix_lints.md)","tokens":300,"sizeBytes":215}],"systemPromptSnippet":"<yakaai_skills repo=\"RunanywhereAI/runanywhere-sdks\">\n<!-- File: AGENTS.md (Tokens: ~6705 | Category: root-instruction) -->\n# AGENTS.md\n\nGuidance for AI coding assistants working in this repository.\n\n> **`AGENTS.md` is the real file; each `CLAUDE.md` is a committed symlink to the\n> `AGENTS.md` beside it.** Editing either name edits the same bytes. If you create a new\n> `AGENTS.md` in a directory that doesn't have one, run\n> `bash scripts/validation/gates/check_agents_claude_sync.sh --fix` from the repo root to\n> create its `CLAUDE.md` symlink — `pr-build.yml` fails any tracked `AGENTS.md` missing one.\n> Never hand-create a `CLAUDE.md` as a real file.\n\n## Working conventions\n\n- **Focus on simplicity.** Clean, SOLID-ish separation of concerns; reuse over\n  reinvention. No mock implementations and no new unit tests unless the task explicitly\n  asks for them.\n- **Plan first for non-trivial work.** Write the plan to\n  `thoughts/shared/plans/{descriptive_name}.md`, get it approved before implementing, and\n  keep it updated (append what changed) as you go so the next engineer can pick it up.\n- **Structured types over raw strings.** Enums/sealed classes/data classes for\n  configuration and results — this is enforced per-SDK by lint (ktlint/detekt, SwiftLint,\n  ESLint) so ad hoc strings tend to get caught anyway.\n- **Read files fully** before editing them; only use offset/limit on files too large to\n  read at once.\n- **Use full local build parallelism** (`-j \"$(sysctl -n hw.logicalcpu)\"` / equivalent),\n  scaling down only under real memory/thermal pressure — not because load average looks\n  high.\n- **Multi-agent workflows run on Sonnet.** When fanning work out to subagents (the\n  `Workflow` tool, or several parallel `Agent` calls), pass `model: 'sonnet'` on every\n  `agent(...)` call rather than letting them inherit the session model. A fan-out of\n  8-10 probes reading large files is exactly the shape where Sonnet is the right\n  cost/latency trade, and these agents are doing bounded, well-specified retrieval and\n  analysis rather than open-ended reasoning. Reserve the session's stronger model for\n  the synthesis step that consumes their reports, and for anything where a wrong answer\n  is expensive and hard to detect — a final design decision, an adversarial verification\n  pass, or a security-relevant judgement.\n\n  ```js\n  // in a Workflow script\n  const findings = await parallel(LENSES.map((l) => () =>\n    agent(l.prompt, { label: `probe:${l.key}`, schema: FINDINGS, model: 'sonnet' })))\n\n  // the synthesis that reads them can stay on the session model\n  const plan = await agent(synthesisPrompt, { schema: PLAN, effort: 'high' })\n  ```\n\n## The most important architectural rule: logic lives at the lowest layer that serves everyone\n\nEach feature/modality (LLM, STT, TTS, VAD, VLM, RAG, LoRA, Voice) is invoked through\n**one** SDK entry point; the SDK, and below it C++ commons, does all the heavy lifting. If\nan example app builds a multi-step bootstrap sequence, hardcodes a model/engine constant,\nor post-processes model output, that is a bug in the SDK, not the app — fix it down a\nlayer.\n\n1. **C++ commons (`core/`)** — cross-platform, non-I/O logic: model lifecycle, registry,\n   download orchestration, RAG session management, inference routing. All 5 SDKs get the\n   fix for free.\n2. **Platform SDK layer** — platform-specific I/O or runtime bridging only (Web OPFS,\n   iOS Keychain, Android Keystore, WASM MEMFS mirroring).\n3. **Example apps** — UI rendering, navigation, thin SDK calls. No business logic, no\n   internal-SDK knowledge (path patterns, framework→directory mappings), no workarounds.\n\n**iOS is the source of truth.** When behavior is ambiguous in any other SDK, check the\niOS Swift implementation first and copy the logic exactly, adapting only syntax.\n\n---\n\n## Repository overview\n\nCross-platform on-device AI SDK monorepo. A single C/C++ core (`runanywhere-commons`,\n~118K first-party LOC plus ~420K generated proto bindings) implements all AI business\nlogic behind a pure C ABI (`rac_*` prefix). Platform SDKs are thin bridges that supply\nplatform services (file I/O, HTTP, Keychain, audio) via an inversion-of-control struct and\ncall into the C core for all inference. Protobuf IDL schemas generate type-safe bindings\nfor every language.\n\n**Current version**: `0.20.37` (canonical source: `core/VERSION`)\n\n| SDK | Path | Bridge mechanism | Platforms |\n|-----|------|-------------------|-----------|\n| Swift | `bindings/swift/` | XCFramework + CRACommons module map | iOS 17.5+, macOS 14.5+ |\n| Kotlin | `bindings/kotlin/` | JNI (`librunanywhere_jni.so`) | Android (min API 24) |\n| Flutter | `bindings/flutter/` | Dart FFI | iOS, Android |\n| React Native | `bindings/react-native/` | NitroModules (JSI HybridObject) | iOS 17.5+, Android arm64 |\n| Web | `bindings/web/` | Emscripten WASM + TypeScript | Chrome, Safari, Firefox |\n| Electron | `bindings/electron/` | Node-API addon | Windows x64 (preview) |\n| Python | `bindings/python/` | pybind11 extension (`runanywhere._core`) | Windows, macOS, Linux (alpha) |\n\nEach has its own `AGENTS.md` with full architecture, build commands, and conventions —\nread it before working in that SDK.\n\n| Native dir | Contents |\n|-----------|----------|\n| `core/` | C/C++ core library: all AI logic, plugin registry, event system |\n| `engines/` | 7 backend plugins: llamacpp, sherpa, onnx, cloud, mlx, qhexrt, neurt |\n| `runtimes/` | 3 runtime adapters: cpu (always), onnxrt, coreml |\n| `idl/` | 39 Protobuf schemas + per-language codegen scripts |\n\n### Consumer apps live in separate repos\n\nThe four full consumer apps were extracted into standalone repos (history preserved); PR\nagainst them there, not here: [runanywhere-ios](https://github.com/RunanywhereAI/runanywhere-ios),\n[runanywhere-android](https://github.com/RunanywhereAI/runanywhere-android),\n[runanywhere-web](https://github.com/RunanywhereAI/runanywhere-web),\n[runanywhere-electron](https://github.com/RunanywhereAI/runanywhere-electron).\n\nTwo example apps remain in-tree (`bindings/flutter/example/`, `bindings/react-native/example/`).\nAll example apps share brand orange `#FF6900`, documented in `docs/DESIGN_GUIDELINE.md`.\n\n### Minimal examples are how you verify an SDK change locally\n\nEach in-repo example consumes its SDK from local source (no staging/publishing needed to\nsee an edit): Swift via `RUNANYWHERE_USE_LOCAL_NATIVES=1`, Kotlin via a Gradle composite\nbuild (`includeBuild` + `dependencySubstitution`), Web via Vite aliases into\n`packages/*/src`. They're deliberately minimal (one prompt in, one streamed completion\nout) — feature-complete UI belongs in the consumer repos above.\n\n---\n\n## Cross-platform architecture\n\n`idl/*.proto` is the schema root; `idl/codegen/generate_all.sh` emits per-language\nbindings (Swift, Kotlin/Wire, TS/ts-proto, Dart), all generated and none committed — see\n[`docs/reference/generated-code-contract.md`](docs/reference/generated-code-contract.md)\nfor the full \"what generates what, when\" contract before touching `idl/`.\n\nAll SDKs reach `runanywhere-commons` through the `rac_*` C API. Commons holds the\ncomponent layer (lifecycle), the service layer (dispatch), and the plugin registry, and\nreaches engines through `rac_engine_vtable_t`.\n\n| Engine | Primitives |\n|---|---|\n| llamacpp | LLM, VLM |\n| sherpa-onnx | STT, TTS, VAD |\n| onnx | Embed, Segment |\n| qhexrt | Hexagon NPU |\n| neurt, cloud | Apple Neural Engine, HTTP |\n\nBackend base priorities (highest wins per primitive via `rac_plugin_find()`, no\nruntime/format scoring): qhexrt=150 (QNN-context models only), mlx=110 (Apple),\nllamacpp=100, sherpa=90, onnx/cloud=50. An explicit engine name is honored through\n`rac_plugin_find_for_engine()` regardless of priority.\n\n### Non-obvious invariants that span every SDK\n\n- **Platform adapter IoC**: `rac_platform_adapter_t` is a flat C struct of function\n  pointers populated by each SDK before `rac_init()`. C++ never calls platform APIs\n  directly — file I/O, HTTP, Keychain, logging, memory queries all pass through it.\n- **Two-phase init**: every SDK does Phase 1 (sync: register adapter, load native libs,\n  configure logging) then Phase 2 (async: authenticate, register device, fetch model\n  assignments, discover downloaded models).\n- **Plugin ABI**: `RAC_PLUGIN_API_VERSION = 9u`; a version mismatch is an immediate\n  rejection. Each vtable has 10 active primitive slots and 7 reserved. NULL slot = not\n  supported. `rerank_ops` (wire value 11) was promoted from `reserved_slot_2` in ABI v8;\n  wire value 6, retired in ABI v4, stays permanently retired — never reuse it. See\n  `core/AGENTS.md` for the full ABI history and how to add a primitive.\n- **Static vs. dynamic plugins**: iOS and WASM force `RAC_STATIC_PLUGINS=ON` (no\n  `dlopen`), registered via `RAC_STATIC_PLUGIN_REGISTER(name)` + `-force_load` /\n  `--whole-archive`. Android/Linux/macOS default to dynamic loading via\n  `rac_registry_load_plugin()`.\n- **Streaming fan-out**: C++ allows only one proto-byte callback per component handle.\n  Each SDK implements its own fan-out to multiplex that into multiple subscribers (Swift\n  `AsyncStream`, Kotlin `Flow`, Dart `StreamController`, TS `AsyncIterable`).\n- **Proto types are canonical**: never hand-write enum values or structured types that\n  exist in `idl/*.proto` — use the generated types/typealiases and regenerate instead.\n- **HTTP is platform-provided**: no libcurl. Each SDK registers a\n  `rac_http_transport_ops_t` (URLSession on Apple platforms, OkHttp on Android,\n  `emscripten_fetch` on Web).\n- **NeuRT crosses a repo boundary as BYTES, not source**: the Apple engine is built and\n  published by the private `neurun` repo and consumed here as prebuilt archives, pinned by\n  tag + SHA-256 in `core/VERSIONS` (`NEURT_*`). Fetch with `scripts/build/download-neurt.sh`.\n  Each archive carries a receipt naming the `RAC_PLUGIN_API_VERSION` it was built against,\n  and both the downloader and CMake refuse a mismatch — a vtable-layout change relinks\n  cleanly and corrupts dispatch at runtime, so nothing else would catch it.\n\nA side-by-side comparison of entry point / bridge / streaming / events / storage / HTTP\nper SDK — useful when porting a fix across SDKs — lives in\n[`docs/reference/cross-sdk-parity.md`](docs/reference/cross-sdk-parity.md).\n\n---\n\n## Building and running\n\nThe root `CMakeLists.txt` (version from `core/VERSION`) is the single entry point for\nnative builds; `CMakePresets.json` defines `macos-{debug,release}`, `linux-{debug,release,asan}`,\n`ios-{device,simulator}`, `android-arm64`, `wasm`, `cpp-desktop-*`, and the `windows-*` presets.\n\n```bash\ncmake --preset macos-debug && cmake --build build/macos-debug && ctest --preset macos-debug\n```\n\n**`./run <group> [subcommand]`** is the unified wrapper for everything else — build,\nlint, and run any SDK or example app from one CLI (`./run --help` for the full menu):\n\n```bash\n./run doctor                    # scan host toolchains, show what's buildable here\n./run setup                     # provision env for every host-buildable target\n./run sdk commons build-android # build C++ commons for all Android ABIs, stage .so files\n./run sdk kotlin build          # (etc. — see per-SDK AGENTS.md for direct tool commands)\n./run example android install   # build + install + launch the Kotlin minimal example\n./run codegen                   # regenerate every language binding from idl/\n```\n\nFor the direct `swift build` / `./gradlew` / `melos` / `yarn` / `npm` commands each SDK\nuses under the hood, and its test/lint/publish targets, see that SDK's own `AGENTS.md`\n(`bindings/<name>/AGENTS.md`) — the commands are kept there, not duplicated here, so they\ncan't drift.\n\nCross-platform build scripts worth knowing about at the root level:\n\n```bash\n./bindings/swift/scripts/build-core-xcframework.sh   # also syncs into RN/Flutter plugin dirs\n./scripts/build/build-core-android.sh                # .so for all ABIs → every SDK's jniLibs/\n./bindings/web/scripts/build-core-wasm.sh\n./scripts/setup/setup-toolchain.sh                   # protoc/wire/ts-proto toolchain\n./idl/codegen/generate_all.sh [--only <lang>]         # swift|kotlin|dart|ts|cpp|python\n```\n\n---\n\n## Version management and releases\n\nCanonical version: `core/VERSION` (single-line semver). Bump everywhere with:\n\n```bash\n./scripts/release/sync-versions.sh <version>\n```\n\nThe full release runbook (version bump → PR → tag → GitHub Release → npm/Maven/pub.dev/\nSwiftPM publishing → cutting the `runanywhere-swift` SPM distribution repo → starter-app\nsmoke tests) is a multi-step process captured in three skills — use them rather than\nimprovising the steps: **sdk-release** (version bump through published GitHub Release),\n**sdk-publish** (registry publishing + the `runanywhere-swift` dist repo cut), and\n**sdk-test-starters** (post-release device/app smoke tests).\n\n### Release pipeline lessons (v0.20.29 through v0.20.33 — read before cutting a release by hand)\n\n- **`native_ios` and `native_android` always rebuild fresh archives on every `release.yml`\n  run — there is no reuse/skip mechanism for either job** (unlike `native_web`'s\n  `reuse_native_web_run_id` or the cpp-desktop kits' `reuse_cpp_desktop_run_id`). This makes\n  the Apple/Android TEMP-pin convention (Package.swift's `sdkVersion`, the Flutter podspecs'\n  `asset_version`, Kotlin's `nativeLibVersion` pinned to an old version \"because this release\n  ships no new iOS/Android archives\") **structurally unsound for any release where those two\n  jobs actually run — which is every release.** It surfaced as three separate bugs\n  (`auto-tag.yml`'s coherence check, `package-sdk.sh`'s podspec check, and `sync-checksums.sh`'s\n  publish-time verification) before the pattern was recognized. If a release genuinely ships\n  no new Apple/Android bytes, that has to be proven by diffing the actual built artifacts\n  against the previous release, not assumed from what changed in the diff.\n- **`RunAnywhereMLXRuntime`'s stripped Mach-O is not byte-reproducible across builds**\n  (`RAC_CHECKSUM_SKIP=RunAnywhereMLXRuntime` in `release.yml`'s publish job exists because of\n  this, and is the ONLY archive exempted from the publish-time checksum gate). The corollary\n  that bit v0.20.29: **the checksum committed to `Package.swift` / the Flutter MLX podspec\n  can only be computed from a build that already happened, but the tag that freezes those\n  files has to be pushed before `native_ios` (which produces that exact artifact) even runs**\n  — so re-syncing checksums against an EARLIER, superseded build (e.g. after a tag had to be\n  moved and re-dispatched to fix an unrelated bug) commits a value that does not match what\n  the FINAL successful run actually publishes, and the skip means nothing catches it. v0.20.29\n  shipped this way (committed `d942788f...`, actually published `fc4b23a0...`) — caught only\n  because the Flutter `runanywhere_mlx` package hadn't been published to pub.dev yet for that\n  version. Before publishing any package that ships `RunAnywhereMLXRuntime`, re-run\n  `bindings/swift/scripts/sync-checksums.sh` against the artifacts from the SPECIFIC run whose\n  tag is being published — never reuse a checksum computed against an earlier attempt.\n- **The Electron QHexRT backend never set `ADSP_LIBRARY_PATH`, and one comment in the\n  codebase confidently asserted Windows doesn't need it.** It does, for exactly one decode\n  path: `QHexRT/src/bonsai/fastrpc_win.cpp`'s custom FastRPC skel (the ternary/Bonsai\n  decoder, e.g. `qwen3.8-27b-1bit-npu`) resolves through `ADSP_LIBRARY_PATH ∪ cwd`, separately\n  from the standard QNN HTP graph path's `LoadLibraryW` + PATH search that `bridge.ts`'s\n  `addSidecarDirToDllSearch` already handled. Every OTHER QHexRT model worked fine in the\n  packaged app, which is exactly what let this ship undetected — validate the Bonsai/ternary\n  path specifically, not just \"some NPU model works.\" Fixed in `bridge.ts`'s\n  `addSidecarDirToDspSearchPath` (#803).\n- **`package-private-engine-overlay.sh`'s QAIRT-runtime copy step filtered on `{\".dll\",\n  \".lib\"}` only**, silently dropping every `.so`/`.cat` skel file (both the standard HTP skel\n  and, since it never read `engines/qhexrt/prebuilt/current/dsp/win-arm64/` at all, the\n  Bonsai skel too). This meant **RCLI could not have run any QHexRT model** — standard or\n  ternary — from a fresh private overlay, a gap invisible until someone actually tried to run\n  inference rather than just building. `bindings/electron/scripts/bundle-native.ts` already\n  had the correct pattern (copy `dsp/win-arm64/` flat, no extension filter); the overlay\n  script just never matched it. Fixed in #804.\n- **A new NeuRT archive must be added in FOUR places, and three of them only fail in a build\n  configuration your Mac never produces.** v0.20.33 added `libneurt_rac_tts_ops.a` and took two\n  wasted release candidates to land, because each site fails somewhere different and late:\n  `engines/neurt/CMakeLists.txt` (probe list + imported target + `_neurt_extra_links`),\n  `build-core-xcframework.sh` (pre-flight guard AND `_NEURT_ROUTABLE_ARCHIVES` — two lists in one\n  file), `package-private-engine-overlay.sh`, and the `#if RAC_NEURT_ROUTABLE` guard around the\n  vtable entry in `rac_plugin_entry_neurt.cpp`. Miss the force-load lists and every native job goes\n  green while `validate_consumer_flutter` dies an hour later on `Undefined symbol:\n  _g_neurt_tts_ops` — the engine registers, all CMake targets compile, and only a CONSUMER's link\n  fails. Miss the vtable guard and the routable build is fine while every PR job WITHOUT\n  `NEURUN_TOKEN` fails, because a non-routable shell still references a symbol nothing defines.\n  A dev machine with the payload staged is routable, so it reproduces neither. `libneurt_rac_*.a`\n  archives now fail packaging by name if they are not in the list\n  (`neurt_assert_no_unlisted_archives`); the vtable guard has no such mechanism — audit all ten\n  slots when adding one. Reproduce a non-routable build by moving `core/third_party/neurt` aside.\n- **`reuse_native_web_run_id` is SAME-VERSION-ONLY, whatever the diff looks like.** `racommons.wasm`\n  embeds `RAC_VERSION_STRING` and `validate_public_packages.py` asserts it against the version being\n  packaged, so reusing ANY prior release's run fails `sdk_web` with *\"does not embed\n  RAC_VERSION_STRING\"*. Worse than a red job: `native_web` is skipped, so that candidate never\n  uploads a web artifact and can never serve as `publish_from_run_id`. Only a re-dispatch at the\n  same version qualifies.\n- **Verify checksums by NAME as well as by URL — the manifests use two different shapes.** The root\n  `Package.swift` interpolates `\\(sdkVersion)` into each URL; the Flutter manifests bind a checksum\n  to a `name:` through a helper with no URL at the call site at all. A URL-only cross-check silently\n  validates 9 of 15 sites and reports success. Re-hash every site against the zip it names, and\n  remember `RAC_CHECKSUM_SKIP` hides `RunAnywhereMLXRuntime` from `publish` entirely, so the Flutter\n  MLX podspec is verifiable only by hand.\n- **A green `rcli backends --json` proves nothing against the OSS kit.** NeuRT ships as a separate\n  private pack, so the public bottle lists no `neurt` at all — a routability check there passes\n  cleanly and means nothing. Apply the private overlay\n  (`cpp-desktop-macos-arm64-neurt-private` workflow artifact) before believing the result.\n- **General pattern across all four of the above**: each one was invisible to the checks that\n  existed (a version-coherence check, a checksum-skip escape hatch, a \"the model loaded\"\n  smoke test, a \"the build succeeded\" CI job) because none of those checks actually ran the\n  specific thing that was broken. When validating a release, run the SPECIFIC flagship/hardest\n  model end-to-end on-device, not just confirm the pipeline completed.\n- **The RACommons/RunAnywhereMLXRuntime checksum-staleness bug above recurred on v0.20.30 AND\n  v0.20.31** — same `mismatch: RACommonsBinary` failure in `publish`, same root cause both\n  times: the version-bump PR ran `sync-versions.sh` and merged/tagged directly, skipping the\n  **mandatory** \"dispatch a release candidate off the unmerged branch, sync checksums from\n  its real build, THEN merge\" sequence that the **`sdk-release` skill's step 2-3 already\n  document in detail** (including this exact failure signature). Documenting the lesson in\n  prose here was not enough to stop it recurring twice more — **if you are about to run\n  `scripts/release/sync-versions.sh` for ANY reason, including as one step of a larger\n  cross-repo release cascade, invoke the `sdk-release` skill and follow it from step 1**,\n  rather than reconstructing the sequence from memory or treating a \"small\" version bump as\n  exempt. v0.20.31 additionally caught `RunAnywhereMLXRuntime` in the Flutter podspec\n  independently stale in the SAME commit — `RAC_CHECKSUM_SKIP` exempts it from `publish`'s\n  check, so it would have shipped broken to Flutter consumers with no CI signal at all; check\n  every row of the checksum-location table, not only the one the CI error names.\n- **QHexRT's `qhx_runtime_device()` (arch/soc lookup, needed to pick the `v75`/`v79`/`v81`\n  manifest directory in `session_open()`) shared its live-device cost with the standard QNN\n  graph path**, so a `host_only` model with ZERO QNN graphs — the Bonsai/Maple ternary decoder\n  (`qwen3.8-27b-1bit-npu`) — paid for a live QNN HTP device anyway, which then contended with\n  the decoder's own direct FastRPC session for the same Hexagon cDSP and made every generation\n  fail with a bare `HostOpFailed`. This shipped in v0.20.30 (the SAME release that fixed the\n  `ADSP_LIBRARY_PATH` bug for the same model) and was found only by running that SPECIFIC model\n  on real Windows-ARM64 hardware after the \"fix\" — reinforcing the point above: a green\n  `HostOpFailed`-free run of a DIFFERENT QHexRT model proves nothing about this one. Two red\n  herrings were chased and ruled out with live device tracing before the real cause: neither\n  `ADSP_LIBRARY_PATH` (confirmed correctly set) nor a QAIRT-version mismatch between the\n  shipped host DLLs and the on-device skel (confirmed present, but fixing it alone did not fix\n  the failure) was the actual cause. Fixed in `neurun` v0.20.31 (`Backend::profile()` now uses\n  a separate, device-handle-free `deviceGetPlatformInfo` query instead of sharing\n  `ensure_device()` with `device()`) + this repo's `qhexrt_session.cpp` (#810, a complementary\n  fix removing a second, redundant eager device query used only for a log line).\n\n---\n\n## CI/CD workflows (`.github/workflows/`)\n\n| Workflow | Trigger | Purpose |\n|----------|---------|---------|\n| `pr-build.yml` | PR to main, push to main/feat branch | Parallel native builds (macOS/Linux/iOS/Android) + per-SDK typecheck + centralization/coherence gates |\n| `release.yml` | Tag `v*.*.*` or manual | Full artifact build matrix, SDK packaging, consumer validation, draft Release |\n| `auto-tag.yml` | PR merged to main with `release:*` label | Verifies the reviewed semver bump, pushes that exact git tag |\n| `idl-drift-check.yml` | Changes to `idl/` or generated files | Regenerates protos, verifies schema lock + untracked generated trees |\n| `legacy-files-blocklist.yml` | All PRs/pushes | Prevents specific deleted files from being re-introduced |\n| `secret-scan.yml` | PRs and pushes to main | Incremental gitleaks scan on diff range |\n| `check-no-pii-logging.yml` | PRs/pushes to main/master/feat-branch | Guards against logging signed URLs alongside active-download destination paths |\n\n---\n\n## Platform requirements\n\n| Platform | Min version | Build tool | Key versions |\n|----------|------------|------------|--------------|\n| iOS / macOS | 17.5 / 14.5 | Xcode 26+ | Swift 6.2 |\n| Kotlin SDK | Android API 24 | AGP 9.2.1 / Gradle 9.5.0 | Kotlin 2.4.0, NDK 27.3.13750724 |\n| Flutter | 3.44.6 | Melos / AGP 9.0.1 / Gradle 9.1.0 | Dart 3.12.2+, NDK 28.2.13676358 |\n| React Native | 0.85.3 (min 0.83.1) | Yarn Berry 3.6.1 | NitroModules, Hermes |\n| Web | Chrome 86+ | Vite | Emscripten 6.0.2, Node 24 LTS |\n| C++ core | N/A | CMake 3.24+ | C++20, Ninja |\n\n---\n\n## Non-obvious configuration details\n\n- **NDK pin**: `core/VERSIONS::NDK_VERSION` (27.3.13750724) is the single source of truth,\n  mirrored into `bindings/kotlin/gradle.properties`. NDK 27 (r27d) gives 16 KB\n  page-alignment required by Android 15+ — NDK 25.x's 4 KB-aligned `libc++_shared.so` /\n  `libomp.so` trips Android 16's page-size enforcement. Mirror this pin whenever bumping\n  it; Flutter/RN Android build files carry their own fallback literals that can drift.\n- **`useLocalNatives`** (Kotlin `gradle.properties`, similarly named flags elsewhere):\n  `true` builds native libs from source locally; CI and most non-local runs set it\n  `false` to download prebuilt `.so`/`.xcframework`/`.wasm` from GitHub Releases instead.\n- **Web SDK** has several hard-won WASM/browser runtime workarounds (VLM worker crash\n  recovery, a Qwen2-VL WebGPU NaN bug, cross-origin isolation requirements) — see\n  [`docs/reference/web-runtime-gotchas.md`](docs/reference/web-runtime-gotchas.md).\n- **Cloud/Linux dev VM** (e.g. Cursor Cloud): what's buildable there and the\n  clang/NDK/KVM gotchas are in\n  [`docs/reference/cursor-cloud-environment.md`](docs/reference/cursor-cloud-environment.md).\n\n---\n\n## Pre-commit hooks\n\n```bash\npre-commit run --all-files                     # everything\npre-commit run ios-sdk-swiftlint --all-files   # SwiftLint only\n```\n\nConfigured hooks: gitleaks (secrets), trailing-whitespace, end-of-file-fixer, check-yaml,\ncheck-added-large-files (1000 KB max), check-merge-conflict, object file detection,\nSwiftLint (SDK + example app), periphery (unused Swift code detection).\n\n---\n\n## Where to look for current in-flight work\n\n`thoughts/shared/plans/` holds active/completed execution plans (including any in-flight\nSDK reorg or migration briefs — e.g. the Electron reorganization plan); `thoughts/shared/issues/`\nholds tracked bug write-ups. Check these directories directly rather than trusting a\nstatus summary written into this file — both change too often for a static snapshot here\nto stay accurate.\n\n<!-- File: core/AGENTS.md (Tokens: ~300 | Category: root-instruction) -->\n# core - AI Agent Protocol & Instructions\n\nPath: `core/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/core/AGENTS.md)\n\n<!-- File: engines/AGENTS.md (Tokens: ~300 | Category: root-instruction) -->\n# engines - AI Agent Protocol & Instructions\n\nPath: `engines/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/engines/AGENTS.md)\n\n<!-- File: idl/AGENTS.md (Tokens: ~300 | Category: root-instruction) -->\n# idl - AI Agent Protocol & Instructions\n\nPath: `idl/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/idl/AGENTS.md)\n\n<!-- File: runtimes/AGENTS.md (Tokens: ~300 | Category: root-instruction) -->\n# runtimes - AI Agent Protocol & Instructions\n\nPath: `runtimes/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/runtimes/AGENTS.md)\n\n<!-- File: scripts/AGENTS.md (Tokens: ~300 | Category: root-instruction) -->\n# scripts - AI Agent Protocol & Instructions\n\nPath: `scripts/AGENTS.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/scripts/AGENTS.md)\n\n<!-- File: .agents/skills/engine-plugin-authoring/SKILL.md (Tokens: ~1903 | Category: anthropic-skill) -->\n---\nname: engine-plugin-authoring\ndescription: Step-by-step recipe for adding a new engine plugin under runanywhere-sdks/engines/, or adding a modality (e.g. TTS) to an existing engine (llamacpp, sherpa, onnx, cloud, mlx, neurt, qhexrt). Use when the task is \"add engine X\", \"wire up backend Y\", or \"make engine Z also serve <modality>\".\n---\n\n# Engine plugin authoring (`engines/`)\n\nTwo related runbooks: adding a brand-new engine plugin, and adding a modality to one\nthat already exists. Read `engines/AGENTS.md` first for the concepts these steps\nassume (op-table vtable, manifest, the valid-engine checklist, the 4-file skeleton,\nthe engine↔runtime rule). This skill is the \"how\", that file is the \"what/why\".\n\n**Invariant that applies to both procedures:** never invent a new modality just to\nship an engine — fill an existing `rac_engine_vtable_t` slot. Adding a brand-new\n*primitive* (a new `RAC_PRIMITIVE_*` + vtable slot) is a commons ABI change, covered\nby `core/AGENTS.md` → \"Adding a new capability interface\", not an engine change.\n\n---\n\n## Procedure A — add a new engine\n\nMirror **sherpa** (multi-modality, standalone `.so`) or **cloud** (single modality,\nno runtime) as your template; both are the cleanest references for the 4-file\nskeleton described in `engines/AGENTS.md`.\n\n1. **Create `engines/<name>/`** with the 4-file skeleton: `rac_plugin_entry_<name>.cpp`,\n   `rac_backend_<name>_register.cpp` (skip it if there's no extra bring-up, like\n   `neurt`), `rac_static_register_<name>.cpp`, plus your impl files.\n2. **Write the manifest** in `rac_plugin_entry_<name>.cpp`:\n   - `name` = snake_case identity (the library/framework you wrap, or your own\n     codebase unit — NEVER a modality name; see the naming rule in\n     `engines/AGENTS.md`).\n   - `primitives[]` = what you serve; fill the matching vtable slots; leave every\n     other slot explicit NULL (the all-NULL tripwire depends on this).\n   - `runtimes[]` — only devices your compute actually depends on (NULL for\n     cloud/HTTP engines) — see THE RULE in `engines/AGENTS.md`.\n   - `formats[]` = the `RAC_MODEL_FORMAT_ID_*` values you accept (NULL if there's\n     no local model file).\n   - `priority`, `availability` (PUBLIC/PRIVATE), `package_owner`/`package_name`.\n3. **Implement the op-table(s)** honoring the MODEL LIFECYCLE:\n   `create → initialize → use → cleanup → destroy` (see `engines/AGENTS.md` for the\n   exact signatures). Use `RAC_DEFINE_CREATE_ADAPTER(primitive, name)`\n   (`core/include/rac/plugin/rac_plugin_entry.h`) when `create` is a plain forward\n   onto `rac_<primitive>_<name>_create` — sherpa uses it for STT/TTS/VAD. Engines\n   with richer create flows (llamacpp, onnx, neurt, qhexrt) hand-write it.\n4. **`capability_check`** when the engine is platform- or binary-gated: use the\n   shared 3-way helper `rac_engine_unavailable_capability(platform_supported,\n   backend_present)` from `engines/common/rac_engine_unavailable.h`. When the\n   backend can be entirely absent at build time (a private prebuilt archive, a\n   sibling private repo), emit the stub with `RAC_ENGINE_UNAVAILABLE_PLUGIN(name,\n   display, cap_fn)` from the same header — see qhexrt and neurt for the two-mode\n   (ROUTABLE vs STUB) pattern this produces.\n5. **Registration carriers:** `RAC_PLUGIN_ENTRY_DEF(<name>)` returning the vtable; a\n   `rac_backend_<name>_register()`/`_unregister()` pair if you have extra bring-up\n   (called directly by SDK bridges on dynamic-link hosts); a\n   `rac_static_register_<name>.cpp` shim using `RAC_STATIC_REGISTER_BACKEND(<name>)`\n   (routes through the register fn) or `RAC_STATIC_PLUGIN_REGISTER(<name>)` (calls\n   the entry directly) for iOS/WASM static hosts.\n6. **CMake:** call `rac_add_engine_plugin(<name> SOURCES … LINK_LIBRARIES …\n   AVAILABILITY … PACKAGE_OWNER … PACKAGE_NAME …)` (`cmake/plugins.cmake`) from\n   `engines/<name>/CMakeLists.txt`, fronted by your own\n   `option(RAC_BACKEND_<NAME> \"…\" <default>)` + `if(NOT RAC_BACKEND_<NAME>)\n   return() endif()` self-gate (each engine owns its own option — `engines/CMakeLists.txt`\n   just unconditionally `add_subdirectory()`s and lets the child decide). Add\n   `add_subdirectory(<name>)` there. Declare `primitives`/`runtimes`/`formats` ONLY\n   in the C manifest, never in CMake, so the build graph can't drift from what the\n   router routes.\n7. **Android:** if the SDK loads the engine via `System.loadLibrary` +\n   `nativeRegister`, add a JNI bridge with `RAC_DEFINE_ENGINE_JNI_BRIDGE(...)`\n   (standalone per-engine `.so` — onnx, llamacpp) or\n   `RAC_DEFINE_ENGINE_JNI_BRIDGE_NO_ONLOAD(...)` (folded into a host lib that\n   already owns `JNI_OnLoad`, e.g. cloud → `librunanywhere_jni.so`) from\n   `engines/common/rac_engine_jni_bridge.h`. The JVM class-path token in the macro\n   call must match the Kotlin `*Bridge` class byte-for-byte.\n8. **iOS/WASM static hosts:** make sure the host force-loads the plugin —\n   `rac_force_load(<app> PLUGINS <name>)` in the host's CMakeLists — so the\n   static-init Registrar TU survives linker stripping.\n9. **If this engine ships an Electron/npm carrier package**, register it in\n   `scripts/validation/gates/check_plugin_natives.py`'s `REQUIRED_OPS` dict (the\n   ops-table symbol name(s) your entry TU references only on the routable\n   branch) — `package-sdk.sh` calls this automatically, so an unregistered\n   engine's stub/shell builds ship undetected. **Check which FILE actually\n   compiles `rac_plugin_entry_<name>.cpp`** before assuming the carrier\n   (`runanywhere_<name>`) is what to gate: sherpa/onnx/llamacpp compile it into\n   the carrier itself (an undefined symbol reference there is the evidence),\n   but neurt's `rac_add_engine_plugin()` call puts it on the ENGINE target\n   (`rac_backend_neurt`) instead — the carrier there is empty boilerplate with\n   zero symbols. Add the engine id to `EVIDENCE_IN_BACKEND_FILE` if it follows\n   neurt's shape; discovered by actually building neurt for real and running\n   `nm -a` on both files, not by reading the CMakeLists and assuming.\n\n## Procedure B — add a modality to an existing engine\n\nMulti-modality is the whole point of an identity-named engine. `cloud` and `neurt`\nare explicitly built for this and carry the recipe inline as a comment next to\ntheir vtable.\n\n1. Implement the new modality's op-table (e.g. a cloud TTS `g_cloud_tts_ops`,\n   typically backed by a per-modality provider adapter under `providers/`).\n2. **Fill the corresponding slot** in the engine's `rac_engine_vtable_t`\n   (`tts_ops` / `llm_ops` / `embedding_ops` / …) — every other slot must stay\n   explicit NULL.\n3. **Add the primitive** to the manifest's `primitives[]`.\n4. Update `formats[]`/`runtimes[]` only if the new modality actually needs them\n   (per THE RULE — don't declare a runtime your new op-table doesn't depend on).\n\nNo new plugin, no rename, no ABI bump — the engine already owns one\n`rac_engine_vtable_t` with all ten primitive slots; you're just filling another one.\nGrep the target engine's `rac_plugin_entry_<name>.cpp` for the phrase \"To add a\"\n— cloud and neurt both leave a comment naming exactly which slots are still free\nand what to add to `primitives[]`.\n\n<!-- File: .agents/skills/sdk-publish/SKILL.md (Tokens: ~10460 | Category: anthropic-skill) -->\n---\nname: sdk-publish\ndescription: Manually publish an already-tagged, already-released (non-draft GitHub Release) RunAnywhere SDK version to every package registry (npm, Maven Central, pub.dev, Swift SPM dist repo) plus macOS rcli notarization, with heavy verification that nothing fake/stubbed/broken ships.\n---\n\n# SDK Publish\n\nRepo: `~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks` (verify with `pwd`/`git remote get-url origin` — do not assume this path is still correct, checkouts move). Version source of truth: `core/VERSION`. Throughout, `$VERSION` means the bare semver being published (e.g. `0.20.24`), never prefixed with `v` except in git tags / GitHub Release names.\n\n## THIS PROCESS IS INTENTIONALLY MANUAL\n\n**`release.yml` never publishes anywhere public.** It only builds artifacts, packages them, and creates a draft/published **GitHub Release**. It does not call `npm publish`, does not upload to Maven Central, does not call `flutter pub publish`, and does not push the Swift dist repo. Verify this is still true before trusting it — greps below should all return **zero** matches (grep the whole file, not a snippet):\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks\ngrep -inE \"npm publish|publishReleasePublication|publishAllPublicationsToMavenCentral|flutter pub publish\" .github/workflows/release.yml\n```\n\nThis is a **deliberate safety gate**, not a gap. Every step below is a human (or an agent acting on a human's explicit go-ahead) pushing a button that CI intentionally never pushes. Do not propose wiring any of this into CI as a \"fix\" — that would remove the gate.\n\n## Precondition check (always run first)\n\nRefuse to proceed if the release is a draft or the asset set looks incomplete.\n\n```bash\ngh release view v$VERSION --json isDraft,tagName --jq '{isDraft, tagName}'\n# isDraft MUST be false.\n\ngh release view v$VERSION --json assets --jq '.assets[].name' | sort\n```\n\nCross-reference the listing against the `sdk-release` skill's step 8 asset-count expectations (`~/.claude/skills/sdk-release/SKILL.md`). As a second, independent check, also diff against the **previous** tagged release's asset list — same count, same name patterns, only the version token differs:\n\n```bash\ngh release view v<PREVIOUS_VERSION> --json assets --jq '.assets[].name' | sed \"s/<PREVIOUS_VERSION>/$VERSION/\" | sort >/tmp/expected.txt\ngh release view v$VERSION --json assets --jq '.assets[].name' | sort >/tmp/actual.txt\ndiff /tmp/expected.txt /tmp/actual.txt\n```\n\nAs of the last verified run (**v0.20.25, 65 assets** — up from 55 at v0.20.24 because the 5 public Electron tarballs and their sidecars joined the release; see §6), the full set is: `MANIFEST.txt`; per-platform `RACommons-{android-arm64-v8a,android-armeabi-v7a,android-x86_64,ios,linux-x86_64,web,windows-x64}-v$VERSION.{zip,tar.gz}` + `.sha256`; iOS backend zips `RABackend{LLAMACPP,MLX,NeuRT,ONNX,Sherpa}-ios-v$VERSION.zip` + `.sha256`; `RunAnywhereMLX{Metal,Resources,Runtime}-ios-v$VERSION.zip` + `.sha256`; `rcli-{linux-x86_64,macos-arm64,windows-x86_64}-v$VERSION.tar.gz`/`.zip` + `.sha256`; the 8 npm tarballs `runanywhere-{core,llamacpp,mlx,onnx,proto-ts,web,web-llamacpp,web-onnx}-$VERSION.tgz` + `.sha256`; the **5 Electron npm tarballs** `runanywhere-electron{,-llamacpp,-onnx,-sherpa,-neurt}-$VERSION.tgz` + `.sha256` (new as of v0.20.25); and `runanywhere-kotlin-maven-v$VERSION.zip` + `.sha256`. There is **no** Flutter tarball in the release (Flutter packages are staged from the native archives above, not from a dedicated release asset) and **no** `.dmg` asset in the baseline set (the macOS rcli tarball is what ships; see the notarization section for when a `.dmg` does get produced). If a `qhexrt`-named asset appears anywhere in this list, stop — that would mean `release.yml`'s own manifest assertion should have hard-failed and something is very wrong; do not publish anything from that release.\n\n## 0. MANDATORY GATE — verify the ENTIRE release before pushing anything to any registry\n\n**Run this once, in full, before the first registry push. Do not publish a single package until it passes.** Per-package spot-checks are not a substitute: they are how a bad artifact reaches a registry you cannot un-publish from. Proven end to end on v0.20.25 (2026-08-22), then independently audited and corrected: **34 payloads, 6619 files across 3 nesting levels, 282 binaries, 0 placeholder hits, 38/38 commons binaries carrying the real staging host**. The audit found 4 methodology gaps in the first pass (shallow nesting, missing `MTLB` magic, `.lib` excluded from the positive check, and a set-intersection that could pass vacuously) — every one is called out inline below, because each produced a *confident but under-covered* result.\n\n```bash\nVERSION=0.20.25                      # no leading v\nREPO=RunanywhereAI/runanywhere-sdks\nW=/tmp/relverify-$VERSION\nrm -rf \"$W\" && mkdir -p \"$W/assets\" \"$W/qhexrt\" \"$W/x\" \"$W/nested\"\n\n# (1) EVERY release asset — no -p filter, get the whole set.\ngh release download \"v$VERSION\" --repo \"$REPO\" -D \"$W/assets\"\n\n# (2) The PRIVATE QHexRT artifact is NOT a release asset — pull it from the run.\n#     Find the run that produced the release, then:\ngh run download <release-run-id> --repo \"$REPO\" --name sdk-electron-qhexrt-private --dir \"$W/qhexrt\"\n```\n\n### (3) Checksums — `cd` into the directory first\n\nSidecars contain a **bare basename**, so `shasum -c` fails with a misleading \"FAILED open or read\" if run from anywhere else. This will make every artifact look broken when nothing is wrong:\n\n```bash\ncd \"$W/assets\"\nok=0; bad=0\nfor s in *.sha256; do\n  if shasum -a 256 -c \"$s\" >/dev/null 2>&1; then ok=$((ok+1)); else echo \"MISMATCH: $s\"; bad=$((bad+1)); fi\ndone\necho \"verified=$ok mismatched=$bad\"      # bad MUST be 0\n(cd \"$W/qhexrt\" && shasum -a 256 -c ./*.sha256)\n```\n\n### (4) Extract everything — INCLUDING nested archives\n\n**The gotcha that matters:** `runanywhere-kotlin-maven-v*.zip` contains `.aar` files, and the `.aar`s are what hold the Android `.so` binaries. A one-level extraction reports \"0 binaries\" for the Maven bundle and every subsequent scan passes **vacuously**.\n\n**Recurse until the tree is dry — TWO levels are required here, not one.** Each `.aar` also contains a `classes.jar`, holding ~2027 `.class` files that are doubly unreachable: inside an unopened jar *and* excluded by the `.class` suffix rule most magic-byte scanners use. Level 2 adds **2033 files** (0 placeholder hits on v0.20.25); level 3 is empty. Loop until a pass finds no new archives rather than hardcoding a depth.\n\n```bash\ncd \"$W/assets\"\nfor f in *; do\n  case \"$f\" in *.sha256|MANIFEST.txt) continue;; esac\n  d=\"$W/x/$f\"; mkdir -p \"$d\"\n  case \"$f\" in\n    *.zip)              unzip -qq -o \"$f\" -d \"$d\" || echo \"EXTRACT FAILED: $f\" ;;\n    *.tar.gz|*.tgz)     tar -xzf \"$f\" -C \"$d\"     || echo \"EXTRACT FAILED: $f\" ;;\n    *) echo \"UNHANDLED TYPE: $f\" ;;\n  esac\ndone\nd=\"$W/x/$(basename \"$W\"/qhexrt/*.tgz)\"; mkdir -p \"$d\"; tar -xzf \"$W\"/qhexrt/*.tgz -C \"$d\"\n\n# nested pass\ncd \"$W/x\"\nfind . -type f \\( -name '*.aar' -o -name '*.jar' -o -name '*.zip' -o -name '*.tgz' -o -name '*.tar.gz' \\) \\\n  | sed 's|^\\./||' | while IFS= read -r a; do\n    dd=\"$W/nested/$(printf '%s' \"$a\" | tr '/' '_')\"; mkdir -p \"$dd\"\n    case \"$a\" in *.aar|*.jar|*.zip) unzip -qq -o \"$a\" -d \"$dd\";; *) tar -xzf \"$a\" -C \"$dd\";; esac\n  done\n```\n\n**Prove extraction actually happened** — an empty dir makes \"0 hits\" meaningless:\n\n```bash\nfor d in \"$W\"/x/*/; do\n  n=$(find \"$d\" -type f | wc -l)\n  [ \"$n\" -lt 2 ] && echo \"SUSPICIOUSLY EMPTY: $d ($n files)\"\ndone\n```\n\n### (5) Placeholder scan — EVERY file, not just binaries\n\nThe staging URL can also live in JS/JSON/TS config, so scan by content across all file types, and additionally scan the raw compressed streams in case a member never got extracted:\n\n```bash\nfind \"$W/x\" \"$W/nested\" -type f | wc -l                                     # total files covered\ngrep -rla \"YOUR_STAGING_BASE_URL\" \"$W/x\" \"$W/nested\" | tee /tmp/ph.txt | wc -l   # MUST be 0\nfor f in \"$W\"/assets/*.tgz \"$W\"/assets/*.tar.gz \"$W\"/qhexrt/*.tgz; do\n  gzip -dc \"$f\" 2>/dev/null | grep -qa \"YOUR_STAGING_BASE_URL\" && echo \"RAW HIT: $f\"\ndone\n```\n\nFind binaries by **magic bytes, not extension** (an unsuffixed framework binary like `RACommons.framework/RACommons` has no extension and a name-based `find` misses it). Signatures to cover:\n\n| Magic | Kind | v0.20.25 count |\n|---|---|---|\n| `7f454c46` | ELF | 177 |\n| `!<arch>` (`213c617263683e0a`) | static `.a` **and `.lib`** | 41 |\n| `MZ` | PE | 29 |\n| `cffaedfe` / `cefaedfe` | Mach-O | 19 |\n| `00 61 73 6d` | wasm | 10 |\n| **`4d544c42` (`MTLB`)** | **Metal library** — easy to miss | 5 |\n| `cafebabe` / `bebafeca` | fat Mach-O (exclude `.jar`/`.class`) | 1 |\n\n**Total: 282 binaries.** An earlier pass reported 277 because `MTLB` was absent from the signature list — `.metallib` files were invisible to the binary scan (they were still covered by the all-file `grep` in step 5, so the conclusion held, but the count was wrong). If your total comes in materially under ~282 for a comparable release, your detection is leaking.\n\n### (6) POSITIVE staging-URL check — absence of the placeholder is NOT sufficient\n\nA build with an **empty** URL baked in passes a placeholder-only check. `check_plugin_natives.py` only tests absence, so add the positive assertion yourself.\n\nTwo traps that produced false confidence on the v0.20.25 pass, both worth avoiding:\n\n- **Include `.lib`.** A `MUST_HAVE` regex of `rac_commons\\.(dylib|so|dll|a)$` silently skips\n  `RACommons-windows-x64/x64/rac_commons.lib` — an **89 MB** binary that then gets no positive\n  check at all. Match `(dylib|so|dll|a|lib)` plus `\\.wasm$`. Correct coverage is **38** binaries,\n  not 37.\n- **Assert per binary, not by set-intersection.** Intersecting \"non-public origins across all\n  files\" yields more than one origin here (`hf.co` *and* the staging host), so a binary carrying\n  only `hf.co` would pass **vacuously**. Check each binary individually for the staging host.\n\n```bash\n# v0.20.25 baseline: 38 commons/wasm/lib binaries; 0 missing the staging host.\n```\n\nNever print the URL — it is a repo secret. Report a truncated SHA-256 fingerprint, or just assert presence/absence counts.\n\n### (7) Routability gate over the whole tree\n\n```bash\npython3 scripts/validation/gates/check_plugin_natives.py \"$W/x\" \"$W/nested\" > /tmp/gate.txt 2>&1\ngrep -cE '^  ok ' /tmp/gate.txt; grep -E '^  FAIL' /tmp/gate.txt\n```\n\nv0.20.25 baseline: **77 ok, 1 FAIL**. The single expected FAIL is\n`RACommons-linux-x86_64/lib/librunanywhere_sherpa.so` — see the known-exception note below. **Any other FAIL blocks the publish.**\n\n### (8) Known exception — Linux sherpa is a non-routable shell (PRE-EXISTING, not a regression)\n\n`RACommons-linux-x86_64-v*.tar.gz` ships **no** `libsherpa-onnx-c-api.so` at all and its `librac_backend_sherpa.so` is a ~156 KB shell (~3 sherpa strings, vs ~31 MB / ~192 for a healthy one on Electron macOS) — the engine is not linked in, so the carrier cannot reference `g_sherpa_{stt,tts,vad}_ops` and **STT/TTS/VAD cannot work from that bundle**. Android's carriers pass on all 3 ABIs, so this is Linux-only.\n\n**Compare the CARRIER, not the backend.** The gate FAILs on `librunanywhere_sherpa.so` (the carrier); citing `librac_backend_sherpa.so` as your evidence is comparing a *different file* and is not proof. The carrier is byte-identical across releases, which is the real proof:\n\n```bash\ngh release download \"v<PREV>\" --repo \"$REPO\" -p 'RACommons-linux-x86_64-v<PREV>.tar.gz' -D /tmp/prev\ntar -xzf /tmp/prev/*.tar.gz -C /tmp/prev\nshasum -a 256 /tmp/prev/lib/librunanywhere_sherpa.so \"$W\"/x/RACommons-linux-*/lib/librunanywhere_sherpa.so\n# v0.20.24 and v0.20.25 both: size=15712  sha256=28ef16015b61da86...\n\n# strongest check — run the gate on the PREVIOUS bundle and confirm the identical FAIL:\npython3 scripts/validation/gates/check_plugin_natives.py /tmp/prev\n```\n\nIf it ever regresses *further* (e.g. Android or macOS sherpa also goes hollow), stop and treat it as a blocking defect.\n\n### (9) Account for every payload explicitly\n\nPrint a per-artifact table (checksum verdict, binary count, placeholder hits) and confirm the row count equals the payload count. Artifacts that legitimately contain **no** native binaries, and why — do not let these pass silently unexamined:\n\n| Artifact | Why it has no binaries |\n|---|---|\n| `RunAnywhereMLXResources-ios-*.zip` | 25 third-party license/notice text files |\n| `runanywhere-kotlin-maven-v*.zip` | binaries live in the **nested** `.aar`s (step 4) |\n| `runanywhere-proto-ts-*.tgz` | pure TS/JS (69 `.ts` + 69 `.js`) |\n\nEverything else must have a non-zero, plausible binary count. A platform suddenly shipping fewer binaries than the previous release is a regression signal worth chasing before publishing.\n\n### (10) Blind spot to be aware of — Windows x64 has NO routability coverage\n\n`RACommons-windows-x64-v*.zip` ships **only** a static `rac_commons.lib` (~89 MB) plus 152 `.h` / 11 `.hpp` headers — **zero backend DLLs**, where Linux/Android/Web each ship 4–5 backend carriers. That is consistent with a static-embedding bundle (consumers link the engines in themselves), but the consequence matters: **no gate can detect a missing or hollow Windows backend**, because there is no carrier to inspect. `check_plugin_natives.py` therefore reports nothing at all for this artifact rather than reporting health.\n\nWindows x64 is also *advisory* in `release.yml` (its absence does not fail the release). So a Windows regression can pass every automated check. If Windows correctness matters for a given release, verify it by actually running something on the real Windows box (see §Electron for how to reach it) — do not infer it from a green pipeline. Worth a deliberate confirmation with the repo owner that the DLL-free shape is intentional.\n\n---\n\n## General verification discipline (every artifact, every registry)\n\nApply all three checks below to **every** file you're about to publish, before AND after the publish call. Never trust a copy cached locally earlier in the session — re-download.\n\n**(a) Download + verify the sha256 sidecar yourself:**\n\n```bash\ngh release download v$VERSION -p '<file>' -p '<file>.sha256' -D /tmp/sdk-publish-$VERSION\ncd /tmp/sdk-publish-$VERSION && shasum -a 256 -c '<file>.sha256'\n```\n\n**(b) Inspect the actual file listing inside every tarball/AAR/zip** — never assume from the filename alone:\n\n```bash\nnpm pack <tgz-or-package-dir> --dry-run     # npm packages: prints the exact file list it would publish\nunzip -l  <file.zip>                        # AARs, Maven repo zip, native archives\ntar -tzf  <file.tar.gz>                     # rcli, RACommons tarballs\n```\n\nSanity-check what comes back:\n- No suspiciously-small or empty binaries where a real native library (`.so`, `.dylib`, `.a`, `.dll`) should be — a stub or a zero-byte placeholder means the build silently failed upstream.\n- No leftover `workspace:*` or `file:` dependency specs in any `package.json` inside the tarball — those only resolve inside this monorepo and break for a real external installer.\n- Version strings inside every manifest (`package.json` `version`, AAR `pom.xml` `<version>`, `pubspec.yaml` `version:`) **exactly** match `$VERSION`.\n- **Hard rule, no exceptions:** grep the full file listing for `qhexrt` (any case) and for QNN binary names (`libQnnHtp`, `QnnHtp*.dll`, `rac_commons.dll` bundled with QHexRT, `runanywhere_qhexrt`). If any hit appears in a package meant to be public, stop and do not publish that artifact.\n\n**(c) Confirm baked-in runtime config** (e.g. a staging-vs-prod backend base URL compiled into `rac_commons` at build time) points at the real intended endpoint. This is designed to fail closed if misconfigured — a build that completed and produces working requests against prod is itself decent evidence — but if you have any doubt, `strings <binary> | grep -i \"api\\.\\|staging\\.\"` and eyeball it.\n\n## 1. npm — 8 packages, order matters\n\nPackages: 4 Web (`@runanywhere/proto-ts`, `@runanywhere/web`, `@runanywhere/web-llamacpp`, `@runanywhere/web-onnx`) + 4 React Native (`@runanywhere/core`, `@runanywhere/llamacpp`, `@runanywhere/mlx`, `@runanywhere/onnx`). Never `@runanywhere/qhexrt` (RN) — it exists in `bindings/react-native/packages/qhexrt/` but is excluded from the public npm train by the same policy as Maven/pub.dev QHexRT.\n\n**Publish `@runanywhere/proto-ts` FIRST.** `@runanywhere/web-onnx` deliberately does not bundle its own `proto-ts` copy and needs it resolvable from the live registry for real consumers; publish `proto-ts` before it or a fresh `npm install @runanywhere/web-onnx` fails to resolve.\n\n```bash\ngh release download v$VERSION -p 'runanywhere-proto-ts-*.tgz*' -p 'runanywhere-web-*.tgz*' \\\n  -p 'runanywhere-core-*.tgz*' -p 'runanywhere-llamacpp-*.tgz*' -p 'runanywhere-mlx-*.tgz*' -p 'runanywhere-onnx-*.tgz*' \\\n  -D /tmp/sdk-publish-$VERSION\ncd /tmp/sdk-publish-$VERSION\nfor f in *.sha256; do shasum -a 256 -c \"$f\"; done\n\nnpm publish runanywhere-proto-ts-$VERSION.tgz --access public   # FIRST, always\nnpm publish runanywhere-web-$VERSION.tgz --access public\nnpm publish runanywhere-web-llamacpp-$VERSION.tgz --access public\nnpm publish runanywhere-web-onnx-$VERSION.tgz --access public\nnpm publish runanywhere-core-$VERSION.tgz --access public\nnpm publish runanywhere-llamacpp-$VERSION.tgz --access public\nnpm publish runanywhere-mlx-$VERSION.tgz --access public\nnpm publish runanywhere-onnx-$VERSION.tgz --access public\n```\n\n`npm whoami` should already show a logged-in account; if it errors, stop and tell the human rather than trying to log in non-interactively.\n\n**GOTCHA — large tarballs can silently die mid-upload.** `@runanywhere/mlx` (~50MB) and `@runanywhere/web-onnx` (unpacks to 300+MB) can exceed a short default shell/tool timeout mid-upload and get killed with no clean error. Use an explicit long timeout (5 minutes / 300000ms) on each publish call.\n\n**GOTCHA — verify via the registry API, not the npm CLI.** `npm view` can lag the CLI's own cache by several seconds right after a fresh publish and falsely report \"not found\". After **every** publish:\n\n```bash\ncurl -s \"https://registry.npmjs.org/@runanywhere/<pkg>\" | python3 -c \"import json,sys;print(json.load(sys.stdin)['dist-tags'])\"\n```\nConfirm `latest` equals `$VERSION`.\n\n## 2. Maven Central — 3 AARs only\n\n`io.github.sanchitmonga22:{runanywhere-sdk,runanywhere-llamacpp,runanywhere-onnx}`. **Never `runanywhere-qhexrt-android`** — it was published through 0.20.19 under an earlier, since-reversed policy and is now permanently excluded; `release.yml`'s manifest assertion hard-fails if it reappears in the public asset set.\n\nFull step-by-step lives in `bindings/kotlin/docs/KOTLIN_MAVEN_CENTRAL_PUBLISHING.md`, section **\"Option A: Publish with released native archives\"** (line ~108 as of this writing — the doc has been corrected mid-release before, so re-read it fresh rather than trusting memory of its contents). Key points, condensed:\n\n1. Download + verify the 3 `RACommons-android-{arm64-v8a,armeabi-v7a,x86_64}-v$VERSION.zip` from the release (sha256 sidecars, per the general discipline above).\n2. Stage: `bash bindings/kotlin/scripts/package-sdk.sh --mode local --natives-from <dir-with-the-3-zips>`. This also produces a local Maven-repo zip — diff its size against `runanywhere-kotlin-maven-v$VERSION.zip` already attached to the GitHub Release as a cheap reproducibility check.\n3. Before uploading, confirm the OSSRH compatibility service has no dangling open repository for this namespace:\n   ```bash\n   CENTRAL_BEARER=\"$(printf '%s:%s' \"$MAVEN_CENTRAL_USERNAME\" \"$MAVEN_CENTRAL_PASSWORD\" | base64 | tr -d '\\r\\n')\"\n   curl -s -H \"Authorization: Bearer $CENTRAL_BEARER\" \\\n     'https://ossrh-staging-api.central.sonatype.com/manual/search/repositories?ip=client&profile_id=io.github.sanchitmonga22'\n   ```\n   Resolve any stale repository per the doc's troubleshooting table before continuing.\n4. Source `MAVEN_CENTRAL_USERNAME`/`MAVEN_CENTRAL_PASSWORD` from `~/.gradle/gradle.properties` (`mavenCentral.username`/`mavenCentral.password` — confirmed present on this machine; never print the values) and run, from `bindings/kotlin/`:\n   ```bash\n   ./gradlew clean \\\n     :publishReleasePublicationToMavenCentralRepository \\\n     :modules:runanywhere-core-llamacpp:publishReleasePublicationToMavenCentralRepository \\\n     :modules:runanywhere-core-onnx:publishReleasePublicationToMavenCentralRepository \\\n     -Prunanywhere.useLocalNatives=true -x buildLocalJniLibs --no-daemon\n   ```\n   GPG signing (`signing.gnupg.keyName`/`signing.gnupg.passphrase` in the same properties file, key id `CC377A9928C7BB18` — confirmed importable via `gpg --list-secret-keys --keyid-format LONG` on this machine) is read from the same properties file automatically; you do not pass it separately.\n5. Transfer to Central Portal:\n   ```bash\n   curl --fail --request POST -H \"Authorization: Bearer $CENTRAL_BEARER\" \\\n     'https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/io.github.sanchitmonga22?publishing_type=automatic'\n   ```\n   Then find the resulting `portal_deployment_id` — expect **exactly one** fresh repository; if the count isn't 1, stop and investigate rather than guessing which one is yours (see the doc for the exact `jq` filter).\n6. Poll `https://central.sonatype.com/api/v1/publisher/status?id=<id>` (POST, same bearer) until `deploymentState` is `PUBLISHED`. Automatic deployments normally go `PENDING -> VALIDATING -> PUBLISHING -> PUBLISHED` within a few minutes. **If it lands on `VALIDATED` instead of proceeding, that means Sonatype is waiting for a manual click in the Central Portal UI** — stop and tell the human; do not loop forever assuming it resolves itself. On `FAILED`, print `.errors` and stop.\n7. Verify propagation (allow 10-30 minutes for CDN propagation even after `PUBLISHED`):\n   ```bash\n   for a in runanywhere-sdk runanywhere-llamacpp runanywhere-onnx; do\n     echo \"$a: $(curl -s -o /dev/null -w '%{http_code}' \"https://repo1.maven.org/maven2/io/github/sanchitmonga22/$a/$VERSION/$a-$VERSION.pom\")\"\n   done\n   ```\n   Expect `200` for all three.\n\n## 3. pub.dev (Flutter)\n\n**Verify the current package set fresh — do not trust a hardcoded count.** As of this writing the real, non-QHexRT Flutter package set is **4**, not 3: `runanywhere`, `runanywhere_llamacpp`, `runanywhere_onnx`, and `runanywhere_mlx` (mlx was added after some earlier documentation/memory of \"3 packages\" was written — that assumption is stale; `runanywhere_mlx` is a real, already-published-on-pub.dev package, not new/WIP). Enumerate it yourself instead of trusting this list verbatim next time:\n```bash\nfind bindings/flutter/packages -maxdepth 1 -type d -not -iname '*qhexrt*' -not -path '*/packages'\n```\n`runanywhere_qhexrt` exists in that directory too — never publish it (same exclusion policy as Maven).\n\nStage the released iOS xcframeworks + Android `.so` into each package with the Flutter equivalent of `package-sdk.sh` (melos-based monorepo, confirmed at `bindings/flutter/scripts/package-sdk.sh`):\n\n```bash\nbash bindings/flutter/scripts/package-sdk.sh --mode local --natives-from <dir-with-xcframeworks-and-android-.so>\n```\n\nThis script already runs `flutter pub publish --dry-run` internally per package as part of its own validation — but run it again yourself, per package, from each package directory, and **read the output for real, not just the exit code**:\n\n```bash\ncd bindings/flutter/packages/runanywhere && flutter pub publish --dry-run\ncd ../runanywhere_llamacpp && flutter pub publish --dry-run\ncd ../runanywhere_onnx && flutter pub publish --dry-run\ncd ../runanywhere_mlx && flutter pub publish --dry-run\n```\n\nExpect zero warnings except a benign version-gap hint. Only then run the real publish, same order (core first, since the others depend on it):\n\n```bash\ncd bindings/flutter/packages/runanywhere && flutter pub publish\ncd ../runanywhere_llamacpp && flutter pub publish\ncd ../runanywhere_onnx && flutter pub publish\ncd ../runanywhere_mlx && flutter pub publish\n```\n\n**Credential note:** pub.dev OAuth is normally cached at `~/Library/Application Support/dart/pub-credentials.json` on macOS (confirmed present on this machine). If it's missing, expired, or `flutter pub publish` prompts for an interactive browser login, **STOP and tell the human** — you cannot complete a fresh Google OAuth flow non-interactively, and forcing/guessing here is not appropriate.\n\nVerify afterward against the live API (not a cached page):\n```bash\nfor p in runanywhere runanywhere_llamacpp runanywhere_onnx runanywhere_mlx; do\n  echo \"$p: $(curl -s \"https://pub.dev/api/packages/$p\" | python3 -c \"import json,sys;print(json.load(sys.stdin)['latest']['version'])\")\"\ndone\n```\n\n## 4. Swift SPM distribution repo (`RunanywhereAI/runanywhere-swift`)\n\nA separate, **generated-only** repo mirroring `bindings/swift` (Package.swift + Sources/ + LICENSE + README) so SwiftPM consumers using `from: \"<version>\"` don't clone the ~340MB monorepo.\n\n**GOTCHA:** this machine may have local directories literally named `runanywhere-swift` that are just checkouts of the **monorepo's** remote, not the distribution repo. Verify before trusting any existing local directory by name:\n```bash\ngit -C <candidate-dir> remote get-url origin\n```\nMust show `RunanywhereAI/runanywhere-swift`, not `runanywhere-sdks`. Always clone fresh instead of guessing:\n\n```bash\ngit clone https://github.com/RunanywhereAI/runanywhere-swift.git /tmp/ra-swift\n\nbindings/swift/scripts/sync-dist-repo.sh \\\n  --zips <exact-verified-Apple-archive-dir-from-the-release> --tag /tmp/ra-swift\n\nRUNANYWHERE_SWIFT_DIST_REPO=/tmp/ra-swift \\\n  bash scripts/validation/gates/check_swift_dist_repo_sync.sh\n\ngit -C /tmp/ra-swift push origin main --follow-tags\n```\n\nThe `--zips` dir must contain the verified (sha256-checked) release archives — `RACommons-ios-v$VERSION.zip`, `RABackend{LLAMACPP,MLX,NeuRT,ONNX,Sherpa}-ios-v$VERSION.zip`, `RunAnywhereMLX{Metal,Resources,Runtime}-ios-v$VERSION.zip` — downloaded and checked exactly per the general verification discipline, not re-used from an earlier, untrusted local copy.\n\n**The tag on `runanywhere-swift` is bare semver, no `v` prefix** (SwiftPM's `from:` needs that) — do not tag it `v$VERSION` by mistake; `sync-dist-repo.sh --tag` handles this correctly, but double check the pushed tag with `git -C /tmp/ra-swift tag --points-at HEAD` before walking away.\n\nThis is enforced, not just documented: once `v$VERSION` is tagged on `runanywhere-sdks`, `check_swift_dist_repo_sync.sh` fails every future PR on the monorepo until `runanywhere-swift` carries the matching bare-semver tag — so don't let this lag behind the other registries.\n\n**GATE GAP — `check_swift_dist_repo_sync.sh` does NOT catch dependency drift, only checksum/version-string drift.** `runanywhere-swift`'s `Package.swift` is manually maintained end to end — `sync-dist-repo.sh` only ever touches `sdkVersion` and the binaryTarget checksums, never the `dependencies:` block (mlx-swift, mlx-swift-lm, mlx-audio-swift, swift-crypto, etc.). If root `runanywhere-sdks/Package.swift` bumps one of those (e.g. a new fork tag), a human has to remember to mirror it into the dist repo by hand, and nothing fails CI if they forget. This exact drift shipped once (2026-08-17): the dist repo pointed `mlx-swift-lm` at unforked `ml-explore/mlx-swift-lm` instead of the `RunanywhereAI` fork, silently breaking every iOS/macOS MLX consumer of that tag until caught by an actual build failure ~40 minutes later. Before trusting a freshly-cut dist-repo tag: diff every `.package(url:` line between root `Package.swift` and the dist repo's `Package.swift` (`grep -n '\\.package(url:' Package.swift` on both sides), and actually build a real product that exercises the dependency you're worried about (e.g. `cd /tmp/ra-swift && swift build --product RunAnywhereMLX`) — don't rely on the sync gate alone. If you catch drift on a tag that's only minutes old with no evidence of external consumption, fixing it in place and force-moving the tag (`git tag -f $VERSION && git push origin $VERSION --force`) is the correct call, not a shortcut — a known-broken immutable tag helps no one.\n\n**If you force-move a tag, SwiftPM/Xcode consumers cache the OLD commit in several places — clearing just `Package.resolved` is not enough.** Verifying your own fix (or having a downstream consumer verify it) can silently keep failing even after the fix is correctly pushed, because: (1) any consumer repo's own tracked `*.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved` pins the exact old resolution and `xcodebuild -resolvePackageDependencies` respects it rather than re-resolving; (2) `~/Library/Caches/org.swift.swiftpm/repositories/<pkg>-<hash>/` is a global shared clone cache keyed by repo, oblivious to a moved tag; (3) `~/Library/org.swift.swiftpm/security/fingerprints/<pkg>-<hash>.json` records the fingerprint SwiftPM saw for that tag and can silently keep using it after a mismatch instead of failing loud. To actually re-resolve after moving a tag: delete the consumer's tracked `Package.resolved`, `rm -rf .build .swiftpm` and any `~/Library/Developer/Xcode/DerivedData/<Scheme>-*`, AND clear the matching entries under both global `org.swift.swiftpm` cache directories for every affected package identity — then resolve fresh.\n\n## 5. macOS rcli notarization\n\nCI (`native_rcli_macos` in `release.yml`) usually already produces a notarized, stapled artifact when Developer ID + notary secrets are configured in repo secrets. **Check the release assets first** before assuming you need to notarize anything yourself locally:\n\n```bash\ngh release view v$VERSION --json assets --jq '.assets[].name' | grep -iE 'rcli-macos|\\.dmg'\n```\nBaseline expectation is `rcli-macos-arm64-v$VERSION.tar.gz` (+ `.sha256`); a `.dmg` only appears when `RCLI_MACOS_FULL_RELEASE=1` was set for that build (see below). If a plain tarball is already present and its contents pass the general verification discipline, there is usually nothing further to do here.\n\n**If you do need to notarize locally**, everything lives in `rcli/scripts/package-rcli.sh` (read its header comment — it documents its own contract in detail):\n\n- One-time setup, check before redoing: `xcrun notarytool history --keychain-profile runanywhere-notary` (confirmed configured on this machine — a fresh submission history returns without error). If missing: `xcrun notarytool store-credentials \"runanywhere-notary\" --apple-id <email> --team-id <team> --password <app-specific-password>`.\n- Codesign identity: `security find-identity -v -p codesigning | grep \"Developer ID Application\"` — confirmed present on this machine as `\"Developer ID Application: RunAnywhere, Inc (<TEAM_ID>)\"`. Grep for it fresh rather than hardcoding the team-id suffix, it can rotate.\n- Invocation contract (env vars, from the script header): `RCLI_CODESIGN_IDENTITY`, `RCLI_MACOS_NOTARIZE=1`, `RCLI_MACOS_FULL_RELEASE=1`, `RCLI_MACOS_SWIFT_BIN_DIR=<output of build-mlx-cli.sh>`. `RCLI_MACOS_FULL_RELEASE=1` is what additionally stages `mlx.metallib` and SwiftPM resource bundles and is what causes a notarized, stapled `.dmg` to be emitted alongside the Homebrew-style tarball. `RCLI_CODESIGN_KEYCHAIN` / `RCLI_NOTARYTOOL_KEYCHAIN` matter only if the identity/profile live in a non-default keychain.\n- Call shape: `rcli/scripts/package-rcli.sh <build-dir> macos-arm64` with the env vars above exported.\n\n## 6. Electron — build AND packaging are now in `release.yml`; publish is still manual\n\n**Reversing the earlier \"deliberately separate\" note**: as of `fix/electron-cicd-staging-url-npu-ane` (2026-08-21), the repo owner asked for Electron to produce ready-to-publish artifacts through the real release train like every other SDK, so `release.yml` now has `native_electron` (invokes `.github/workflows/electron-native-package.yml` as a reusable `workflow_call`) and `sdk_electron` (downloads its `electron-packaged-tarballs` artifact, runs `scripts/release/prepublish_check.py` on every tarball, checksums them, and splits public vs. QHexRT-private). `publish` hard-gates on `sdk_electron` succeeding. `@runanywhere/electron`, `-llamacpp`, `-onnx`, `-sherpa`, `-neurt` land as real, `.sha256`-sidecarred GitHub Release assets, same as the Web/RN npm tarballs. `@runanywhere/electron-qhexrt` still never becomes a public Release asset (policy below) — it's produced and packaged every release, but only reachable as the `sdk-electron-qhexrt-private` workflow artifact on that run, for a human to `npm publish` by hand. Electron is **not** WIP on a branch anymore: the old root `AGENTS.md` \"Active issues\" section describing `smonga/electron_upgrade` as in-progress is gone entirely as of this checkout — `bindings/electron/` is a fully merged, real SDK on `main`. Re-check that section is still absent before trusting this note.\n\n`.github/workflows/electron-native-package.yml` itself is unchanged in what it builds: every backend for real (llamacpp/onnx/sherpa/neurt on their real platforms, qhexrt on a self-hosted Windows ARM64 runner) with the real `STAGING_BASE_URL` baked in, gated by `check_plugin_natives.py` (which fails closed on the staging-URL placeholder — the exact bug that shipped through `0.20.24`). What's new is that it's also invocable via `workflow_call`, and that `native_win_x64_and_package` now tolerates a failed/skipped QHexRT lane (self-hosted, single machine) rather than losing the other five real packages to one runner's outage. **`release.yml` itself still does not `npm publish` anything** — it only produces artifacts (Release assets for the public five, a workflow artifact for QHexRT) — same \"build automated, publish manual\" shape as every other platform. See `thoughts/shared/plans/electron_cicd_staging_url_and_npu_ane_pipeline.md` for the full history.\n\n**PROVEN END TO END on v0.20.25 (2026-08-22)** — the first release where this actually ran. Final state: 65 release assets, all 5 public Electron tarballs present with `.sha256` sidecars, zero `qhexrt`-named assets. Verified by downloading the *published* tarballs and running the repo's own gate against them:\n\n```bash\ngh release download v<version> --repo RunanywhereAI/runanywhere-sdks -p 'runanywhere-electron*.tgz' -D /tmp/verify\npython3 scripts/validation/gates/check_plugin_natives.py --expect-version <version> \\\n    $(for t in /tmp/verify/*.tgz; do printf -- '--tarball %s ' \"$t\"; done)\n# => All 20 check(s) passed: plugin natives are routable and no staging-URL placeholder was found.\n```\n\nThat single command asserts all three things you actually care about, per binary: the real `STAGING_BASE_URL` is baked in (no `YOUR_STAGING_BASE_URL`), `rac_commons` embeds the right version, and every backend is genuinely **routable** (`g_llamacpp_ops`, `g_sherpa_stt_ops`, `g_neurt_llm_ops`, `g_onnx_embeddings_ops`, …) rather than a same-sized shell. **Run it before every Electron `npm publish`** — it is the cheapest defense against exactly the class of defect that shipped `electron-sherpa` 0.20.17's NULL-vtable carrier. Expected shipped layout: entry package = `darwin-arm64` + `win32-x64` + `win32-arm64`; llamacpp/onnx/sherpa = `darwin-arm64` + `win32-x64`; neurt = `darwin-arm64` only; qhexrt = `win32-arm64` only.\n\nWhere to get the QHexRT tarball for its manual publish (it is deliberately NOT a release asset):\n\n```bash\ngh run download <release-run-id> --repo RunanywhereAI/runanywhere-sdks \\\n    --name sdk-electron-qhexrt-private --dir /tmp/qhexrt\n```\n\nIf asked to actually publish an Electron package (still a real, separate, manual step — this skill's general discipline in §0/§General verification still applies in full):\n- Involve the Windows box for real Windows-native verification before publishing anything Windows-targeted. The team keeps a **Windows ARM64 machine reachable over SSH**; its hostname, user, key, and mesh-VPN node name are operator-specific and deliberately not recorded in this repo — read the Windows/ARM64 host entry out of the operator's `~/.ssh/config` and use that alias. If there is no such entry, the box is not configured for you: say so and ask, rather than guessing at a hostname. Check reachability **first** by asking the mesh-VPN client whether the node is up (e.g. `tailscale status`, matching on the host entry's `HostName`) — if it reports `offline, last seen ...`, the box is asleep, which is not a configuration problem: ask the human to wake it, do not debug it as broken. Only then `ssh -o ConnectTimeout=8 \"$WIN_BOX\" \"whoami\"`. That same machine is **also** registered as a GitHub Actions self-hosted runner (labels `[self-hosted, Windows, ARM64, qhexrt]`) running as a Windows service — a queued `native_win_arm64_qhexrt` job on it means the machine is busy; check `gh api repos/RunanywhereAI/runanywhere-sdks/actions/runners` for the matching runner's `busy`/`status` before assuming it's free for manual SSH work, and vice versa.\n- **`@runanywhere/electron-qhexrt` being public on npm is a CONFIRMED, settled policy decision** (repo owner, 2026-08-20) — it contains real Hexagon NPU binaries (`libQnnHtpV81Skel.so`, `QnnHtp*.dll`, `rac_commons.dll`, `runanywhere_qhexrt.dll`) on purpose. The QHexRT-stays-private policy enforced everywhere else in this skill is specifically about the **GitHub-Release-side** asset set (Android etc.), a different channel — not a contradiction. Stop treating this as an unresolved item to flag; only re-raise it if you find evidence the policy changed.\n\n## 7. Final sanity pass\n\nAfter every registry is done, re-view each package's public listing/API and compare against the previous version — a big unexplained drop in file count or package size is a red flag to investigate before calling the release fully published:\n\n```bash\n# npm\nfor p in proto-ts web web-llamacpp web-onnx core llamacpp mlx onnx; do\n  npm pack @runanywhere/$p@$VERSION --dry-run 2>&1 | tail -5\ndone\n\n# Maven — check module metadata / POM sizes are non-trivial, not near-empty\nfor a in runanywhere-sdk runanywhere-llamacpp runanywhere-onnx; do\n  curl -s -o /dev/null -w \"%{size_download} bytes  $a\\n\" \\\n    \"https://repo1.maven.org/maven2/io/github/sanchitmonga22/$a/$VERSION/$a-$VERSION.aar\"\ndone\n\n# pub.dev\nfor p in runanywhere runanywhere_llamacpp runanywhere_onnx runanywhere_mlx; do\n  curl -s \"https://pub.dev/api/packages/$p\" | python3 -c \"import json,sys;d=json.load(sys.stdin);print('$p', d['latest']['version'])\"\ndone\n```\n\nOnly after this pass — every registry shows `$VERSION`, every artifact's contents check out, and nothing regressed in size/file-count versus the previous release — is the release fully published. Do not skip this step just because each individual publish call reported success; a successful upload is not the same claim as \"the right bytes are now live.\"\n\n<!-- File: .agents/skills/sdk-release/SKILL.md (Tokens: ~13119 | Category: anthropic-skill) -->\n---\nname: sdk-release\ndescription: Cut a new RunAnywhere SDK release end-to-end in RunanywhereAI/runanywhere-sdks — version bump through a fully green, tagged, drafted-then-published GitHub Release. Stops BEFORE publishing to package registries (npm/Maven/pub.dev/SwiftPM dist) — that is the separate \"sdk-publish\" skill, invoked as the last step here.\n---\n\n# SDK Release (runanywhere-sdks)\n\nRepo: `~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks` (verify with `pwd`/`ls` —\nthis is a workspace subfolder, not the repo root of the whole Qualcomm/ tree, and paths\ndrift; re-check script paths below still exist before trusting them blindly).\n\nThis is a **runbook**, not a description. Follow the steps in order. Every `gh`/`git`\ncommand below is meant to be run close to verbatim — adjust only the version/PR/run-id\nvalues for the release you're actually cutting.\n\n**THIS SKILL HAS BEEN SKIPPED THREE TIMES IN A ROW (v0.20.29, v0.20.30, v0.20.31),\nproducing the IDENTICAL failure each time, DESPITE step 2/3 below documenting the exact\nfix since the v0.20.25/v0.20.29 incidents.** All three times, an agent ran\n`sync-versions.sh`, opened the PR, merged it, and pushed the tag directly — skipping the\n\"dispatch a release candidate BEFORE merging, sync checksums from it, THEN merge\" sequence\nin steps 2-3 — usually because the version bump was one step inside a longer autonomous\nchain (a downstream repo re-pin, a multi-repo release cascade) and felt like \"just a\nversion bump,\" not a release. **A version bump to `core/VERSION` IS a release. If you are\nabout to run `scripts/release/sync-versions.sh` for ANY reason — including re-pinning this\nrepo as a step in some OTHER repo's release — stop and follow this skill from step 1,\nincluding the candidate dispatch. Do not improvise the sequence from memory even if you\ndid it correctly last time; do not skip it because \"this bump is small\" or \"we're in a\nhurry.\"** v0.20.31 additionally caught a second, previously-undetected instance of the\nsame class of bug in the SAME commit: `RunAnywhereMLXRuntime` in the Flutter podspec (which\n`RAC_CHECKSUM_SKIP` exempts from CI verification, so it fails silently for a consumer\ninstead of failing `publish`) was ALSO stale, and had to be independently re-verified\nagainst the same candidate build — see step 3's checksum-location table; check every row\nin it, not just the one CI complained about.\n\nCompanion skill: **sdk-publish** — invoke it at the very end (step 10) for the actual\nnpm / Maven / pub.dev / SwiftPM-distribution publishing. This skill's job stops at a\npublished (non-draft) GitHub Release with correct assets.\n\n---\n\n## 0. Orientation facts (confirm these still hold before relying on them)\n\n- Canonical version file: `core/VERSION` (single line, e.g. `0.20.24`). `core/VERSIONS`\n  also carries a `PROJECT_VERSION=` line that must agree — `sync-versions.sh` updates\n  both, `auto-tag.yml` reads `core/VERSIONS`, `check_release_version_coherence.sh` reads\n  `core/VERSION`. Don't hand-edit either; always go through the script.\n- `scripts/release/sync-versions.sh <version>` — bumps ~20 files (Package.swift,\n  gradle.properties, all package.json/pubspec.yaml, Kotlin/Swift/TS version constants,\n  AGENTS.md). Accepts a leading `v` and strips it.\n- `scripts/validation/gates/check_release_version_coherence.sh` — the gate that enforces\n  \"PR changing VERSION must carry exactly one `release:patch|minor|major` label, and the\n  new version must be exactly base+1 for that bump type.\" Reads `PR_BASE_SHA` and\n  `PR_RELEASE_LABELS_JSON` env vars when run under CI; locally it only checks the\n  canonical-version-format part.\n- `.github/workflows/pr-build.yml` job **`centralization`** is what actually runs the\n  coherence gate in CI (`Release-train version coherence` step), alongside\n  `check_typescript_centralization.sh`, `check_flutter_centralization.sh`,\n  `check_gradle_centralization.sh`, `check_wasm_provenance_contract.sh`,\n  `check_swift_dist_repo_sync.sh`, `check_agents_claude_sync.sh`,\n  `check_no_hardcoded_defaults.sh`. This job's trigger types are\n  `[opened, synchronize, reopened, labeled, unlabeled]` — **adding the release label\n  after opening the PR fires a fresh run**, which is how the gate clears without a new\n  commit (see step 1).\n- `.github/workflows/release.yml` — the build/package/draft-release workflow. Triggers:\n  tag push `v*.*.*` OR `workflow_dispatch` (works on **any branch**, not just tags) with\n  inputs `version` (required), `validate_external_starters` (bool, default false),\n  `publish_from_run_id` (optional), `reuse_native_web_run_id` (optional).\n- `.github/workflows/auto-tag.yml` — fires on `pull_request: closed` to `main` when the\n  merged PR carries a `release:*` label. Verifies the version bump matches the label,\n  pushes the `v<version>` tag, and **explicitly dispatches a fresh `release.yml` run at\n  that tag** (plain `git push` of a tag with `GITHUB_TOKEN` does not auto-trigger\n  workflows, so it calls `gh workflow run release.yml --ref v<version> -f version=<version>`\n  itself). This is the redundant rebuild you must cancel — see step 7.\n- Branch protection on `main`: check\n  `gh api repos/RunanywhereAI/runanywhere-sdks/branches/main/protection` — 2 approvals +\n  code owner review required, but look for a `bypass_pull_request_allowances` (users/teams/apps)\n  list. If your `gh api user --jq .login` is on that list, `gh pr merge --squash --admin`\n  is a legitimate, pre-configured bypass — not a hack. If not on the list, stop and ask a\n  human to approve/merge.\n- Never let any `qhexrt`-named asset reach the public asset set. `release.yml`'s own\n  manifest-assertion step (`find release-flat -maxdepth 1 -type f -iname '*qhexrt*'`)\n  hard-fails the run if one is found — this is enforced, not just policy.\n- **Electron IS part of `release.yml` now** (as of `fix/electron-cicd-staging-url-npu-ane`,\n  2026-08-21) — this reverses the prior \"deliberately separate\" policy noted below by an\n  earlier pass; the repo owner explicitly asked for Electron to produce ready-to-publish\n  artifacts through the real release train like every other SDK. `native_electron`\n  invokes `.github/workflows/electron-native-package.yml` as a **reusable workflow**\n  (`workflow_call`) rather than duplicating its ~400 lines of hard-won native-build logic\n  inline; `sdk_electron` downloads the resulting `electron-packaged-tarballs` artifact,\n  runs `scripts/release/prepublish_check.py` on every tarball, computes checksums, and\n  splits public vs. QHexRT-private before anything reaches `release-flat`. `publish`\n  hard-gates on `sdk_electron` succeeding (same tier as `sdk_kotlin`/`sdk_web`/\n  `sdk_react_native`) but treats `native_electron`'s own result as advisory, because that\n  job's result folds in `native_win_arm64_qhexrt` — a single, personally-owned\n  self-hosted NPU runner whose outage should cost only `@runanywhere/electron-qhexrt`\n  for that release, not the whole SDK train (`native_win_x64_and_package`, the job that\n  actually produces the tarballs, tolerates a failed/skipped QHexRT lane and still packages\n  the other five real artifacts). Like every other SDK here, this still does **not**\n  `npm publish` anything — it only produces the tarballs as GitHub Release assets\n  (public ones) / a workflow artifact (the private QHexRT one) for a human to publish by\n  hand, matching the \"build automated, publish manual\" shape everywhere else.\n  **Electron is no longer WIP on a branch** — the old root `AGENTS.md`/`CLAUDE.md`\n  \"Active issues\" section describing `smonga/electron_upgrade` as in-progress is gone\n  entirely as of this checkout; `bindings/electron/` is a fully merged, real SDK on\n  `main` with its own `AGENTS.md`. Re-check that section is still absent before trusting\n  this note — if it has reappeared, something regressed.\n  See `thoughts/shared/plans/electron_cicd_staging_url_and_npu_ane_pipeline.md` for the\n  full history (this pipeline shipped the placeholder-URL bug three times before\n  `electron-native-package.yml` existed).\n  **QHexRT public/private boundary — CONFIRMED POLICY, not an open flag**: the repo\n  owner explicitly confirmed (2026-08-20) that `@runanywhere/electron-qhexrt` stays\n  public on npm intentionally (it already is, and is a real, working precedent worth\n  studying before touching QHexRT packaging elsewhere) — the QHexRT-stays-private\n  policy enforced everywhere else in this pipeline is specifically about the\n  **GitHub-Release-side** asset set (e.g. Android), a different distribution channel.\n  This is settled; stop re-flagging it as an unresolved contradiction. Only re-raise it\n  if you find evidence the policy has changed.\n\n---\n\n## 0b. NeuRT is a PINNED PREBUILT now — check this before anything else\n\n`native_ios` no longer compiles the private `neurun` repo. It downloads archives that\nrepo publishes, pinned by tag + SHA-256 in `core/VERSIONS`, the same shape as sherpa-onnx:\n\n```\nNEURUN_REPO=RunanywhereAI/neurun      # shared: one repo publishes BOTH engines\nNEURT_RELEASE_TAG=v<version>          # per-engine: a release may carry one lane only\nNEURT_RAC_ABI_VERSION=9\nNEURT_MACOS_ARM64_SHA256=…\nNEURT_IOS_ARM64_SHA256=…\nNEURT_IOS_ARM64_SIMULATOR_SHA256=…\n```\n\n`NEURUN_REPO` is shared because one neurun release carries both engines. The **tags stay\nper-engine** on purpose: that repo can cut a release for one lane only, so a given tag may\ncarry just one engine's assets.\n\n**Run the gate after re-pinning** — it verifies the pins against the release itself, so a\nmistyped or locally-computed value fails there instead of mid-build:\n\n```bash\nNEURUN_TOKEN=\"$(gh auth token)\" bash scripts/validation/gates/check_engine_prebuilt_pins.sh\n```\n\n### Local engine development — there is no source mode\n\nThe engine has no `NEURT_ROOT` path any more; nothing here compiles engine source. To iterate\non NeuRT, build a slice in the neurun checkout and point the engine at it — no release, no\ntag, no pin edit:\n\n```bash\n# in neurun\nNeuRT/tools/scripts/package-rac-dist.sh --sdk-root <this repo> --version 0.0.0-dev \\\n    --slice macos-arm64 --out /tmp/devdist\nmkdir -p /tmp/devslice && tar -xzf /tmp/devdist/neurt-macos-arm64-v0.0.0-dev.tar.gz \\\n    -C /tmp/devslice --strip-components=1\n\n# here\nNEURT_PREBUILT_ROOT=/tmp/devslice cmake -S . -B build/dev -G Ninja \\\n    -DRAC_BUILD_BACKENDS=ON -DRAC_BACKEND_NEURT=ON -DRAC_RUNTIME_COREML=ON\n# expect: Mode: PREBUILT (macos-arm64, ABI 9)\n```\n\n`NEURT_PREBUILT_ROOT` is a **single-slice** root, which is why\n`build-core-xcframework.sh` deliberately ignores it: packaging needs all three slices, so\nhonouring a single-slice override there would let one staged slice satisfy the guard and\nsilently package two non-routable stubs. Use an obviously-not-a-release version like\n`0.0.0-dev` so a dev slice can never be mistaken for a published one.\n\n**Why this changed, and why it matters to you specifically.** `engines/neurt/CMakeLists.txt`\nused to resolve a `NEURT_ROOT` checkout of that private repo and **regex-parse its root\n`CMakeLists.txt`** for a source list. Under that arrangement, commons widened\n`rac_llm_stream_callback_fn` with `tokens_in_delta`, the pinned neurun commit still passed\n4 arguments, and `native_ios` died with *\"too few arguments to function call, expected 5,\nhave 4\"* on **v0.20.15, v0.20.16 and v0.20.17**. Every packaging job needs `native_ios`,\nso all three shipped **iOS-only — 18 assets where a healthy release has 55+**. That is the\nsingle most expensive failure this runbook exists to prevent, and it came from this seam.\n\n**Before opening the release PR**, confirm the pin is current:\n\n```bash\ngrep -A5 '^NEURT_REPO' core/VERSIONS\ngh release view \"$(grep '^NEURT_RELEASE_TAG=' core/VERSIONS | cut -d= -f2)\" \\\n    --repo RunanywhereAI/neurun --json assets --jq '.assets[].name'\n```\n\nExpect six assets — three `neurt-<slice>-v<version>.tar.gz` plus their `.sha256` sidecars.\n\n**If the plugin ABI changed this cycle** (`RAC_PLUGIN_API_VERSION` in\n`core/include/rac/plugin/rac_plugin_entry.h`), the pinned archives are invalid and\n`native_ios` will fail at the download step. Cut a neurun release first — that repo's\n`release_sdk_archives` skill is the runbook — then re-pin both `NEURT_RELEASE_TAG` and\n`NEURT_RAC_ABI_VERSION` here. The downloader refuses to run if `NEURT_RAC_ABI_VERSION`\ndisagrees with the header, so this cannot be half-done.\n\nProve it locally before trusting a tag build:\n\n```bash\nNEURUN_TOKEN=\"$(gh auth token)\" bash scripts/build/download-neurt.sh --all\n```\n\nThat verifies each tarball's SHA-256 **and** that its `RECEIPT.json` names the same ABI\nversion this repo defines. Then check configure prints `Mode: PREBUILT (<slice>, ABI N)`.\n`SOURCE` means a local neurun checkout was picked up (prebuilt is meant to win, so the\ndownload did not land); `SHELL` means the engine is **not routable** and a release built\nnow would ship an engine that registers, claims Apple availability and serves nothing.\n\n`release.yml` treats the downloader's exit **3** (\"no token — degrade to the shell\") as\n**fatal**. That degrade is right for a fork PR and catastrophic for a release.\n\n### The complete engine matrix — who consumes what, and how\n\nThere are only **three fetch points in the entire repo**. Everything else consumes an\nsdks artifact, which is why a single fetch fix can repair five SDKs at once — and why a\nsingle missing fetch can silently break several.\n\n```\nneurun release (private)\n  ├─ native_ios         ──NeuRT x3 slices──►  RABackendNeuRT.xcframework\n  │                                              ├─ Swift  (Package.swift binary target)\n  │                                              ├─ Flutter / React Native (co-vendored\n  │                                              │   in the onnx package's podspec)\n  │                                              └─ rcli-macos (stages it, then GREPS\n  │                                                  `backends --json` for neurt)\n  ├─ native_android     ──QHexRT arm64-v8a──►  one build stages jniLibs into\n  │                                              Kotlin + React Native + Flutter\n  └─ electron           ──NeuRT macos-arm64 AND QHexRT win-arm64──►  its own natives\n```\n\n| target | NeuRT | QHexRT | note |\n|---|---|---|---|\n| Swift (iOS/macOS) | ✅ xcframework | — | NeuRT is Apple-only |\n| Flutter / React Native (Apple) | ✅ xcframework | — | co-vendored in the onnx package |\n| Flutter / React Native (Android) | — | ✅ jniLibs | from the same Android build as Kotlin |\n| Kotlin (Android) | — | ✅ jniLibs | arm64-v8a only; the only ABI with a Hexagon NPU |\n| Electron macOS | ✅ fetches | — | builds its own natives |\n| Electron win-arm64 | — | ✅ fetches | |\n| rcli macOS | ✅ xcframework | — | **best check in the tree** (below) |\n| rcli linux / windows-x64 | — | — | correct: NeuRT is Apple-only, QHexRT is win-arm64 |\n\n**`rcli-macos` is the strongest verification that exists here.** It greps\n`backends --json` for `\"name\":\"neurt\"` — and a non-routable shell *refuses\nregistration*, so that grep proves the engine is **routable**, not merely present. Copy\nthis pattern when adding an engine rather than checking for a file on disk.\n\n**Two silent-failure traps this matrix exists to prevent:**\n\n- **`RAC_BACKEND_<X>=ON` does not mean the engine works.** Without a payload the engine\n  builds as a non-routable shell and registration is refused — no build error. Android\n  shipped that way for months, and `rcli-macos-release` still sets\n  `RAC_BACKEND_NEURT: ON` regardless of whether a payload was fetched.\n- **`copy_if_exists` in `build-core-android.sh` silently no-ops** when a lib was never\n  built. That is the same guarded-copy pattern that shipped Windows with zero engines for\n  several releases. The release now asserts `librunanywhere_qhexrt.so` is really inside\n  the arm64-v8a archive.\n\n`scripts/validation/gates/check_engine_prebuilt_pins.sh` asserts all of the above plus\nthe pins, the ABI, header drift against the receipt, and (with a token) the release\ncontents. Run it before cutting a release; it is also wired into `pr-build` offline.\n\n### QHexRT is pinned the same way — check it too\n\nThe Hexagon-NPU engine crosses the identical boundary, published by the same neurun release:\n\n```\nQHEXRT_RELEASE_TAG=v<version>\nQHEXRT_ARM64_V8A_SHA256=…      QHEXRT_ARM64_V8A_RECEIPT=…\nQHEXRT_WIN_ARM64_SHA256=…      QHEXRT_WIN_ARM64_RECEIPT=…\n```\n\nTwo pins per ABI, not one: the tarball checksum **and** the payload's own build-receipt\nhash. The receipt is the directory name under `engines/qhexrt/prebuilt/versions/`, so\npinning it is what makes the pin describe the bytes rather than trusting the archive's\nown claim about itself.\n\n```bash\nNEURUN_TOKEN=\"$(gh auth token)\" bash scripts/build/download-qhexrt.sh --abi arm64-v8a\nNEURUN_TOKEN=\"$(gh auth token)\" bash scripts/build/download-qhexrt.sh --abi win-arm64\n```\n\n**`current` selects ONE ABI at a time.** Both payloads can sit in `versions/` together, but\na build targets one platform — so re-run the downloader when switching between an Android\nand a Windows build. The downloader ends by running `validate-qhexrt-prebuilt.py` itself,\nso a bad payload fails at the pin rather than mid-build.\n\n**The QHexRT lane in neurun is deliberately non-blocking.** It runs on a single\nself-hosted Snapdragon box (QAIRT is licence-gated and exists on no hosted runner). If\nthat machine is offline, neurun's release publishes the Apple artifacts and **warns** that\nno QHexRT payload was produced — and the SDK simply keeps its previously pinned prebuilt.\nThat is correct behaviour, not a failure: do not block an SDK release on it. Only treat it\nas blocking if this release is specifically meant to ship new NPU kernels.\n\n**Do not hand-stage `engines/qhexrt/prebuilt/`.** That was the old flow, and it is how the\nElectron win-arm64 lane once pinned a receipt staged months earlier and silently shipped\nNPU kernels from an old commit. If the tree looks stale, re-pin and re-download; never\ncopy a payload in by hand.\n\n---\n\n## 1. Decide the version and open the release PR\n\nCheck for an already-open release PR first — don't duplicate work:\n\n```bash\ngh pr list --repo RunanywhereAI/runanywhere-sdks --label release:patch --label release:minor\n```\n\n(gh ORs multiple `--label` flags into a single AND-filtered query per invocation is not\nguaranteed across all gh versions — if unsure, run the two label queries separately:\n`--label release:patch` then `--label release:minor`, plus `--label release:major`.)\n\nPick a patch/minor/major bump based on the changes since the last tag. Then:\n\n```bash\ngit checkout -b release/v<version>\n./scripts/release/sync-versions.sh <version>      # e.g. 0.20.25 (no 'v' needed, either works)\nbash scripts/validation/gates/check_release_version_coherence.sh\n```\n\nThe coherence script may point out generated locks/changelogs that need regenerating\n(e.g. package-lock.json version bumps it expects but didn't find, or a missing CHANGELOG\nheading for the new version). Fix everything it flags, and add **explicit, human-written\nchangelog entries** — the gate requires a heading for the new version to exist; it does\nnot write prose for you.\n\nOpen the PR and **apply exactly one `release:patch` or `release:minor` (or `release:major`)\nlabel** — this is required, not optional. Two failure modes to know:\n\n- If you forget the label and push a commit later, the `centralization` job's\n  `Release-train version coherence` step fails.\n- **Adding the label after the PR is already open does NOT get picked up by the run that\n  already executed.** Because `pr-build.yml`'s `centralization` job triggers on\n  `labeled`/`unlabeled` PR events, applying the label fires a **new** run of the whole\n  job — that new run is what goes green. Don't push an empty commit to \"retrigger\" it;\n  just add/re-add the label (or use `gh pr edit <pr> --add-label release:patch`, and if it\n  was already present, remove + re-add to force the event).\n\n---\n\n## 2. Get REAL native-artifact checksums BEFORE merging — dispatch a release candidate\n\nDo not merge on faith that native builds will succeed. `release.yml`'s\n`workflow_dispatch` trigger works on **any branch**, including your unmerged release\nbranch — use it to build a full release candidate first:\n\n```bash\ngh workflow run release.yml --ref release/v<version> -f version=<version>\n```\n\nGet the run id and watch it:\n\n```bash\ngh run list --repo RunanywhereAI/runanywhere-sdks --workflow=release.yml --limit 5\n```\n\n**Do not block synchronously waiting on it.** Load the `Monitor` tool (or a background\npoll loop) to watch for completion/failure, and keep doing other release prep (changelog\nreview, CodeRabbit thread triage, branch-protection check) in the meantime. Keep\nnotifications to real state changes only — don't ping on every intermediate job\ncompleting, only on the run's overall failure or full completion.\n\n### REAL job durations — measured, not guessed. NEVER cancel a job for being \"slow\".\n\nMeasured on the 0.20.25 train (2026-08-21/22). A release train legitimately takes\n**4-5 hours** wall-clock when `native_web` has to build.\n\n| Job | Real duration | Notes |\n|---|---|---|\n| **`native_web`** (release.yml) | **~3h51m** | The long pole, by far. Its `Vendor ONNX Runtime and Sherpa-ONNX WASM archives` step alone is **~71 minutes** — it BUILDS onnxruntime + sherpa to WASM from source; it is not a git clone. Then 4 more WASM builds: CPU core+llamacpp+onnx ~89m, WebGPU llamacpp ~37m, WebGPU onnx+sherpa ~33m. |\n| `native_ios` | ~60-75 min | XCFrameworks + MLX. No longer compiles neurun — it downloads the pinned NeuRT archives (see §0b), which is seconds, not minutes. |\n| `swift-spm` (pr-build) | **69-85 min** | `Build XCFramework` step is most of it. Measured across 6 consecutive `main` runs (2026-08-21/22): 69, 78, 75, 85, 77, 69. A run sitting at ~78 minutes is **healthy**, not hung — check this row before reaching for cancel. |\n| `kotlin-android` (pr-build) | ~40 min | |\n| `python-windows` (pr-build) | ~40 min | |\n| `electron (windows-2022)` (electron-sdk-ci) | ~40 min | |\n| `rcli-windows` | ~30 min | |\n| `native_electron / native_win_arm64_qhexrt` | ~28 min | Self-hosted; may sit **queued** for a long time first if the single runner is busy. Queued ≠ hung. |\n| `native_electron / native_win_x64_and_package` | ~35-45 min | |\n| `wasm` (pr-build) | ~12 min | **Do NOT use this as a proxy for `native_web`.** Totally different scope. |\n\n**The mistake this table exists to prevent**: `pr-build.yml`'s `wasm` job takes ~12\nminutes, so a ~50-minute `native_web` \"vendor\" step looks hung. It is not. On the 0.20.25\ntrain this misread caused two healthy `native_web` jobs to be cancelled at 2h19m and\n~55m, wasting ~3 hours and two full candidate builds. Before concluding \"hung\", check\nstep-level progress and compare against THIS table:\n\n```bash\ngh run view <run-id> --repo RunanywhereAI/runanywhere-sdks --json jobs \\\n    --jq '.jobs[] | select(.name==\"native_web\") | {startedAt, steps: [.steps[] | select(.status==\"in_progress\") | .name]}'\n```\n\n### `reuse_native_web_run_id` is the correct way to avoid re-paying that ~4 hours\n\nIf a run needs redoing and **nothing under `bindings/web/wasm/`, `engines/`, or `core/`\nchanged** since a run whose `native_web` succeeded, reuse it instead of rebuilding:\n\n```bash\ngit diff --stat <good-run-sha> <current-sha>      # prove the scope first\ngh workflow run release.yml --ref release/<version> \\\n    -f version=<version> -f reuse_native_web_run_id=<good-run-id>\n```\n\n`native_web` then shows `skipped`, and `sdk_web` + `validate_consumer_web` still run and\npass against the reused artifact — which is also your proof the reuse was valid. Track the\nrun id that **originally built** it: that is the id you keep citing (see the chaining note\nin step 7).\n\n---\n\n## 3. Sync checksums once native_ios finishes\n\nPre-tag, `gh release download` will not work (there is no release yet) — pull the run's\nartifact directly:\n\n```bash\ngh run download <candidate-run-id> --repo RunanywhereAI/runanywhere-sdks \\\n    --name native-ios-macos --dir release-artifacts/native-ios-macos\n```\n\nVerify every `.zip` against its `.sha256` sidecar **yourself** before trusting it:\n\n```bash\ncd release-artifacts/native-ios-macos\nfor f in *.zip; do shasum -a 256 -c \"${f}.sha256\" || echo \"MISMATCH: $f\"; done\ncd -\n```\n\nThen sync:\n\n```bash\nbindings/swift/scripts/sync-checksums.sh release-artifacts/native-ios-macos\n```\n\n**KNOWN FACT — do not treat this as a bug**: `RunAnywhereMLXRuntime` (and occasionally\n`RACommons`) are **not byte-reproducible** between rebuilds of identical source. Expect\ntheir checksums to differ from any prior candidate build every single time you rebuild,\neven with zero source changes. Only investigate if a checksum for a framework that\nhistorically IS reproducible (e.g. plain XCFrameworks with no Swift codegen step)\nsuddenly changes.\n\n**Where each checksum actually lives — these are NOT all in one file:**\n\n| Manifest | Carries |\n|---|---|\n| root `Package.swift` | `RACommons` + the 5 backend XCFrameworks (6 checksums total) |\n| nested Flutter `Package.swift` (under `bindings/flutter/`) | same 6 |\n| `bindings/flutter/packages/runanywhere_mlx/ios/runanywhere_mlx.podspec` → `mlx_checksums` hash | `RunAnywhereMLXRuntime`, `RunAnywhereMLXMetal`, `RunAnywhereMLXResources` — **only here**, nowhere else |\n\nAfter `sync-checksums.sh` runs, verify centralization didn't drift and re-run the version\ncoherence gate locally:\n\n```bash\nbash scripts/validation/gates/check_flutter_centralization.sh\nbash scripts/validation/gates/check_release_version_coherence.sh\n```\n\nCommit and push the checksum-sync commit to the release branch:\n\n```bash\ngit add Package.swift bindings/flutter/Package.swift \\\n    bindings/flutter/packages/runanywhere_mlx/ios/runanywhere_mlx.podspec\ngit commit -m \"release: sync v<version> checksums from candidate run <candidate-run-id>\"\ngit push\n```\n\n### THIS STEP IS NOT OPTIONAL — skipping it fails `publish` after the tag already exists\n\n**What happens if you skip it** (observed on the 0.20.25 train): every build and package\njob goes green, `publish` gets all the way past flatten and the asset-manifest assertion,\nthen dies at **`Verify built Apple checksums match tagged package contracts`**:\n\n```\nmismatch: RACommonsBinary\n  Package.swift: 0609dae6...      release zip: 1ae55ae4...\nmismatch: RABackendLlamaCPPBinary\n  Package.swift: 0597442f...      release zip: 7062f7c8...\n>> Done. 8 processed, 0 missing, 4 failed.\nERROR: built Swift archives do not match the immutable tagged manifest\n```\n\nNote `8 processed / 4 failed` is really **2 distinct archives counted twice** (root\nmanifest + Flutter manifest each). On that train `RACommons` and `RABackendLLAMACPP`\ndrifted while ONNX/Sherpa/NeuRT/MLX/MLXMetal/MLXResources all matched — so \"6 of 8 passed\"\nis a normal-looking partial failure, not evidence of something exotic.\n\n**Recovery once the tag already exists** (this is safe and was done on 0.20.25):\n\n1. Confirm nothing was actually published — a failed `publish` dies *before* creating the\n   release, so there is usually nothing to undo:\n   ```bash\n   gh release view v<version> --repo RunanywhereAI/runanywhere-sdks   # expect \"release not found\"\n   ```\n2. Download the **exact artifacts the release will publish** (the candidate run named in\n   `publish_from_run_id`), verify each against its sidecar, and sync on a branch off `main`.\n3. Merge that branch, then **re-point the tag** at the new commit. `git push origin --delete`\n   can hang on flaky networks; the API is more reliable:\n   ```bash\n   gh api -X DELETE repos/RunanywhereAI/runanywhere-sdks/git/refs/tags/v<version>\n   gh api -X POST   repos/RunanywhereAI/runanywhere-sdks/git/refs \\\n       -f ref=\"refs/tags/v<version>\" -f sha=\"<new-main-sha>\"\n   ```\n   Only do this while **no release exists and nothing has been published**. If a release\n   was already published, do NOT move the tag — cut the next patch version instead.\n4. **Creating a tag via the API DOES trigger `release.yml`'s `push` trigger** (unlike a\n   `GITHUB_TOKEN` git-push of a tag, which does not). So immediately after re-tagging you\n   will have TWO runs: a full rebuild from the `push` event, and your own\n   `workflow_dispatch`. Identify by event and cancel the `push` one:\n   ```bash\n   gh run list --repo RunanywhereAI/runanywhere-sdks --workflow=release.yml --limit 3 \\\n       --json databaseId,event,headBranch --jq '.[] | \"\\(.databaseId) \\(.event) \\(.headBranch)\"'\n   gh api -X POST repos/RunanywhereAI/runanywhere-sdks/actions/runs/<push-run-id>/cancel\n   ```\n\nCheapest prevention: run `sync-checksums.sh --check <zip_dir>` against the candidate's\nartifacts and require **`N processed, 0 missing, 0 failed`** before you merge the release\nPR at all.\n\n**Recurred on v0.20.30 and again on v0.20.31** — same `mismatch: RACommonsBinary` /\n`mismatch: RACommons` failure, same root cause (step 2's candidate dispatch skipped\nentirely both times), fixed the same way both times: compute the real checksum from the\nalready-built release-artifact zip (`gh run download <failed-run-id> --name\nnative-ios-macos`), commit it on a fix branch, merge, then move the tag (§10d) to\nre-trigger `release.yml`. On v0.20.31 this ALSO surfaced the `RunAnywhereMLXRuntime`\npodspec entry as independently stale — `RAC_CHECKSUM_SKIP` means `publish` never checks\nit, so it would have shipped broken to Flutter consumers silently. **When fixing this\nfailure after the fact, check every row of the checksum-location table in step 3, not\nonly the archive named in the CI error** — the ones `RAC_CHECKSUM_SKIP` exempts are\nexactly the ones that won't tell you.\n\n---\n\n## 4. Watch for the \"package intentionally doesn't bundle a sibling dep\" bug class\n\nThis exact release hit a real bug in `bindings/web/scripts/package-sdk.sh`:\n`@runanywhere/web-onnx` deliberately does **not** bundle its own `proto-ts` copy (only\n`core`/`llamacpp` do — enforced by `bindings/web/scripts/validate_public_packages.py` and\nmirrored in `bindings/react-native/scripts/validate_public_packages.py`), but the\npackaging script's install-smoke-test (`verify_candidate_install`, around line 165 of\n`package-sdk.sh`) never supplied a resolution target for that unbundled dependency, so\n`npm install` reached out to the real npm registry for a not-yet-published version and\nfailed with `ETARGET`.\n\nFix pattern (already applied, keep it this way): the `onnx` call to\n`verify_candidate_install` passes the just-built local `proto-ts` tarball\n(`$PROTO_ARCHIVE`, built via `npm pack ../proto-ts`) as an explicit extra install arg —\nsee lines ~209-233 of `bindings/web/scripts/package-sdk.sh`, where `core` and `llamacpp`\ncalls omit it (they bundle proto-ts themselves) but `onnx`'s call appends `\"$PROTO_ARCHIVE\"`.\n\n**Generalize this as a standing check for every release**: grep for the asymmetric\nbundling flags before trusting any packaging script —\n\n```bash\ngrep -n \"expects_bundled_proto\\|--bundle\" \\\n    bindings/web/scripts/package-sdk.sh bindings/react-native/scripts/package-sdk.sh\n```\n\nWhenever a package intentionally does NOT bundle a sibling first-party dependency, its\nlocal pre-publish verification step must supply that dependency some other way (a local\ntarball) — the real registry version genuinely does not exist yet at candidate-build\ntime, and letting `npm`/`yarn` hit the real registry will `ETARGET`.\n\n**Before trusting any packaging script is bug-free, actually run it** against the real\ndownloaded native artifacts and check the exit code — do not just read the code and\nassume it's fine:\n\n```bash\ncd bindings/web && ./scripts/package-sdk.sh   # then check $?\ncd bindings/react-native && ./scripts/package-sdk.sh\n```\n\n### 4b. The two Electron-specific release bugs (both fixed — know the shapes)\n\nThe 0.20.25 train was the **first** release to run Electron packaging inside `release.yml`,\nand it surfaced two defects that no local test or green PR check could have caught. Both\nare fixed; recognize the shapes because they generalize.\n\n**(1) `sync-versions.sh` has a hardcoded package list, and new packages get missed.**\n`bindings/electron/packages/neurt/package.json` sat at the previous version while every\nsibling moved, because `neurt` was added (electron#734) after that list was last touched.\nSymptom: `native_win_x64_and_package` fails with\n\n```\nERROR: npm pack did not produce .../runanywhere-electron-neurt-<version>.tgz\n```\n\n— because `npm pack` correctly produced `...-neurt-<OLD-version>.tgz` from the stale\nmanifest. **Whenever you add a package anywhere, add it to `sync-versions.sh` the same\nday.** Cheap pre-flight check that catches the whole class:\n\n```bash\ngrep -h '\"version\"' bindings/electron/package.json bindings/electron/packages/*/package.json \\\n    bindings/electron/native/package.json | sort -u     # expect exactly ONE distinct version\n```\n\n**(2) `publish` downloads EVERY artifact, including intermediate hand-off bundles.**\n`publish`'s `Download all artifacts` step has no name filter. `electron-packaged-tarballs`\nis `native_win_x64_and_package`'s raw output whose only purpose is feeding `sdk_electron`,\nwhich re-uploads the curated public set as `sdk-electron`. Flattening both duplicated all\n5 Electron tarballs *and* dragged in the raw bundle's own `proto-ts` copy, which collided\nwith `sdk_web`'s:\n\n```\n::error::flatten collision: release-flat/runanywhere-proto-ts-<v>.tgz already exists\n::error::Refusing to publish — 6 filename collision(s) detected during flatten\n```\n\nFixed by pruning that directory in the flatten `find`\n(`-type d -name \"electron-packaged-tarballs\" -prune -o -type f \\( ... \\)`). **Standing\nrule: any new intermediate artifact must be either excluded from the flatten or named so\nit cannot collide.** When editing that `find`, test the exact expression against a fake\n`release-artifacts/` tree locally first — `-prune -o` + `-print0` is easy to get subtly\nwrong and the only other way to find out costs a full release run.\n\n---\n\n## 5. Resolve CodeRabbit (or any bot) review findings properly\n\nDo not resolve a review thread without a real, evidence-backed reply. Steps:\n\n1. Fetch unresolved threads via GraphQL:\n\n```bash\ngh api graphql -f query='\nquery($owner:String!, $repo:String!, $pr:Int!) {\n  repository(owner:$owner, name:$repo) {\n    pullRequest(number:$pr) {\n      reviewThreads(first:100) {\n        nodes { id isResolved comments(first:10) { nodes { id databaseId body } } }\n      }\n    }\n  }\n}' -f owner=RunanywhereAI -f repo=runanywhere-sdks -F pr=<pr-number>\n```\n\n2. For each unresolved thread, reply via REST with `in_reply_to` set to the comment's\n   `databaseId` (not the GraphQL node id), citing concrete evidence — a real run id,\n   a real checksum, a real commit SHA, not \"looks fine\":\n\n```bash\ngh api -X POST repos/RunanywhereAI/runanywhere-sdks/pulls/<pr-number>/comments \\\n    -f body=\"Verified against candidate run <run-id>: RunAnywhereMLXRuntime checksum\nmatches release-artifacts/native-ios-macos/RunAnywhereMLXRuntime-ios-v<version>.zip.sha256.\nSee commit <sha>.\" \\\n    -F in_reply_to=<comment-database-id>\n```\n\n3. Only then resolve, via GraphQL mutation on the thread id from step 1:\n\n```bash\ngh api graphql -f query='\nmutation($threadId:ID!) { resolveReviewThread(input:{threadId:$threadId}) { thread { isResolved } } }\n' -f threadId=<thread-node-id>\n```\n\n---\n\n## 6. Merge\n\nBefore merging, verify:\n\n```bash\ngh pr view <pr-number> --repo RunanywhereAI/runanywhere-sdks --json mergeable,reviewDecision\n```\n\nZero unresolved review threads (re-check the GraphQL query from step 5 — it should return\nnone with `isResolved: false`), and `mergeable == \"MERGEABLE\"`.\n\nConfirm bypass eligibility before relying on `--admin`:\n\n```bash\ngh api repos/RunanywhereAI/runanywhere-sdks/branches/main/protection \\\n    --jq '.required_pull_request_reviews.bypass_pull_request_allowances'\ngh api user --jq .login\n```\n\nIf your login appears in that allowance list (users, or a team/app you belong to):\n\n```bash\ngh pr merge <pr-number> --repo RunanywhereAI/runanywhere-sdks --squash --admin\n```\n\nIf not on the list, **stop** — ask a human to approve or merge instead of forcing it.\n`--admin` is a legitimate configured bypass only for allow-listed identities; do not use\nit to route around review for anyone else.\n\n---\n\n## 7. GOTCHA — cancel the auto-tag-dispatched rebuild, dispatch a publish-only run instead\n\nThe moment the PR merges, `auto-tag.yml` runs, pushes the `v<version>` tag, and **itself**\ndispatches a brand-new full `release.yml` run at that tag (see orientation section — plain\ntag pushes via `GITHUB_TOKEN` don't auto-trigger, so the workflow does it explicitly).\nThis is redundant — you already have a fully green, checksum-verified candidate from step\n2-3 — and wasteful, because Apple/MLX bytes will drift again for zero benefit (see the\nnon-reproducibility fact in step 3).\n\n**Cancel it immediately, before it burns real compute** (ideally within the first minute\nor two, before native_ios starts real work):\n\n```bash\ngh run list --repo RunanywhereAI/runanywhere-sdks --workflow=release.yml --limit 5\ngh api -X POST repos/RunanywhereAI/runanywhere-sdks/actions/runs/<auto-triggered-run-id>/cancel\n```\n\nThen dispatch a **publish-only** run that reuses your already-verified candidate's exact\nbytes instead of rebuilding:\n\n```bash\ngh workflow run release.yml --ref v<version> \\\n    -f version=<version> \\\n    -f publish_from_run_id=<candidate-run-id>\n```\n\n**CRITICAL SUB-GOTCHA**: if your candidate run in step 2 itself passed\n`reuse_native_web_run_id=<X>` to skip rebuilding WASM (a packaging-only re-release), that\ncandidate run **never uploaded its own copy of the `native-web` artifact** — it was\nskipped. `publish_from_run_id` alone will then fail during the release-manifest assertion\nwith `\"Missing or empty Web WASM payload\"`, because the publish job's artifact-download\nstep only pulls `reuse_native_web_run_id`'s artifact when that input is passed to *this*\ndispatch too (see `.github/workflows/release.yml` around the \"Download reused Web natives\"\nstep — it is gated on `inputs.reuse_native_web_run_id != ''`, independent of\n`publish_from_run_id`). You must **also** pass the same original run id:\n\n```bash\ngh workflow run release.yml --ref v<version> \\\n    -f version=<version> \\\n    -f publish_from_run_id=<candidate-run-id> \\\n    -f reuse_native_web_run_id=<X>\n```\n\nThis chains forward across releases: whichever run **originally** built `native_web` is\nthe run you keep citing at every subsequent `reuse_native_web_run_id`, not the most recent\nrun that merely borrowed it from an earlier one. Track that original run id explicitly if\nyou're doing back-to-back packaging-only releases.\n\n---\n\n## 8. Verify the draft release's asset set\n\nThe publish job only creates a **draft** GitHub Release (`softprops/action-gh-release`,\n`draft: true`) with built assets attached — it does not touch npm/Maven/pub.dev at all\n(grep `release.yml` for `npm publish`, `mavenCentral`, `pub publish` — none exist there).\n\nExpected asset count for a full, non-Windows-focused release: **~65 assets** —\n\n- 9 Apple archives (iOS/macOS XCFrameworks: RACommons + 5 backends + MLX runtime/Metal/\n  resources + CoreML/NeuRT)\n- 3 Android RACommons zips (arm64-v8a, armeabi-v7a, x86_64)\n- 1 Linux RACommons tar.gz\n- 1 Windows RACommons zip (preserved as an asset, advisory — see step 9)\n- 1 Web WASM tar.gz (`RACommons-web-v<version>.tar.gz`)\n- 3 rcli tarballs (macOS arm64, Linux x86_64, Windows x86_64 — macOS may also have a\n  notarized `.dmg` if Developer ID/notary secrets are configured; this is soft-skipped\n  otherwise, not a failure)\n- 1 Kotlin Maven repo zip\n- 8 npm tarballs (proto-ts, web core, web-llamacpp, web-onnx, RN core, RN llamacpp, RN mlx,\n  RN onnx)\n- 5 Electron npm tarballs (`runanywhere-electron-<v>.tgz` core, `-llamacpp`, `-onnx`,\n  `-sherpa`, `-neurt`) — added by `sdk_electron`; each has its own `.sha256` sidecar\n  counted separately below. **`electron-qhexrt` is deliberately excluded** — it still\n  gets built and packaged, but only as the `sdk-electron-qhexrt-private` workflow\n  artifact (not a release asset); download it separately for a manual `npm publish`.\n- 1 `MANIFEST.txt`\n- **explicitly ZERO** `qhexrt`-named files (the manifest assertion step hard-fails the run\n  if it finds any — confirm the run actually reached the publish job and didn't die there)\n\nEvery asset above except `MANIFEST.txt` has a `.sha256` sidecar, which is itself a\nseparate asset. **v0.20.25 landed at exactly 65 assets** — use that as the concrete\nbaseline rather than re-deriving the arithmetic.\n\nCross-check every checksum's `.sha256` sidecar against what got committed to\n`Package.swift` / the flutter `Package.swift` / the MLX podspec in step 3 — they must\nmatch byte for byte, because this run's artifacts are exactly what step 10 (sdk-publish)\nships to real consumers.\n\n```bash\ngh run view <publish-run-id> --repo RunanywhereAI/runanywhere-sdks --json jobs \\\n    --jq '.jobs[] | {name, conclusion}'\n\n# Asset count + the Electron set + the qhexrt-leak assertion, in three commands:\ngh release view v<version> --repo RunanywhereAI/runanywhere-sdks --json assets --jq '.assets|length'\ngh release view v<version> --repo RunanywhereAI/runanywhere-sdks --json assets \\\n    --jq '[.assets[].name | select(startswith(\"runanywhere-electron\"))] | sort'\ngh release view v<version> --repo RunanywhereAI/runanywhere-sdks --json assets \\\n    --jq '[.assets[].name | select(test(\"qhexrt\";\"i\"))]'          # MUST be []\n```\n\n### Prove the artifacts are real — don't trust green checkmarks\n\nA green `publish` only proves the assertions ran. To prove the bytes are right, run the\nrepo's own fail-closed gate against the **published** tarballs. This is the single highest\n-value verification in the whole runbook and it takes about a minute:\n\n```bash\ngh release download v<version> --repo RunanywhereAI/runanywhere-sdks \\\n    -p 'runanywhere-electron*.tgz' -D /tmp/verify\npython3 scripts/validation/gates/check_plugin_natives.py --expect-version <version> \\\n    $(for t in /tmp/verify/*.tgz; do printf -- '--tarball %s ' \"$t\"; done)\n```\n\nExpect `All N check(s) passed: plugin natives are routable and no staging-URL placeholder\nwas found.` On v0.20.25 that was **20 checks** across 5 tarballs, asserting three separate\nthings per binary: the real `STAGING_BASE_URL` is baked in (no `YOUR_STAGING_BASE_URL`\nplaceholder), `rac_commons` embeds the right version string, and each backend is actually\n**routable** (real op tables — `g_llamacpp_ops`, `g_sherpa_stt_ops`, `g_neurt_llm_ops`,\netc.), which is what distinguishes a real plugin from a same-sized shell.\n\nSanity-check the shipped layout too — a tarball missing a platform is a silent regression:\n\n| Package | Expected `prebuilds/` |\n|---|---|\n| `@runanywhere/electron` | `darwin-arm64` + `win32-x64` + `win32-arm64` |\n| `-llamacpp`, `-onnx`, `-sherpa` | `darwin-arm64` + `win32-x64` |\n| `-neurt` | `darwin-arm64` only (Apple Neural Engine) |\n| `-qhexrt` (private artifact) | `win32-arm64` only (Hexagon NPU) |\n\n---\n\n## 9. Windows handling — build/preserve only, never auto-publish\n\n`native_windows` and `native_rcli_windows` run and their outputs are **preserved** as\nrelease assets (proving the binaries build), but nothing Windows-specific is published to\nany package registry as part of the standard flow unless a human explicitly asks for it —\nthat's a deliberate, separate decision, not an oversight.\n\nFor anything that needs actual execution on Windows (smoke-testing the native Windows\nbuild, or Electron's Windows packaging), do not assume a macOS cross-build proves\nWindows correctness — SSH to the real Windows box. The team keeps a **Windows ARM64\nmachine reachable over SSH**; its hostname, user, key, and mesh-VPN node name are\noperator-specific and deliberately not recorded in this repo — read the Windows/ARM64\nhost entry out of the operator's `~/.ssh/config` and use that alias. If there is no such\nentry, the box is not configured for you: say so and ask, rather than guessing.\n\n```bash\nWIN_BOX=<the ssh alias from ~/.ssh/config>\n# Ask the mesh-VPN client whether the node is up FIRST (e.g. `tailscale status`,\n# matching on that host entry's HostName) — never just try SSH and retry on timeout.\nssh -o ConnectTimeout=8 \"$WIN_BOX\" \"whoami\"\n```\n\nIf the VPN reports the node offline/asleep, **that's not a config problem** — ask the\nuser to wake the box; don't debug SSH config against a machine that's simply off.\n\n---\n\n## 10. Publish the draft, then hand off\n\nOnce the draft's asset set is verified complete and correct (step 8):\n\n```bash\ngh release edit v<version> --repo RunanywhereAI/runanywhere-sdks --draft=false --latest\n```\n\nThis is the point where SwiftPM's `from: \"<version>\"` resolution and the download URLs\nbaked into `Package.swift` start actually working for real external consumers — draft\nrelease assets are not publicly downloadable, so nothing before this line is\nexternally live.\n\n**Hand off to the `sdk-publish` skill** for the actual npm / Maven Central / pub.dev /\nSwiftPM-distribution-repo publishing steps. This skill's job ends here.\n\n---\n\n## 10b. MANDATORY, and it turns `main` RED until you do it — cut runanywhere-swift\n\nThe moment `v<version>` exists in this repo, `check_swift_dist_repo_sync.sh` (run by\n`pr-build.yml`'s `centralization` job) **fails on every PR and every push to `main`**:\n\n```\n[FAIL] runanywhere-swift has no 0.20.25 tag (latest: 0.20.24)\n       v0.20.25 is published here, so 'from: \"0.20.25\"' is broken for every\n       SwiftPM consumer of https://github.com/RunanywhereAI/runanywhere-swift.git\n```\n\nThis is **by design**, not a bug, and it is the #1 reason a release looks \"not green\"\nafter everything else succeeded. Do not go hunting for a code defect — cut the repo:\n\n```bash\ngit clone https://github.com/RunanywhereAI/runanywhere-swift.git /tmp/ra-swift\nbindings/swift/scripts/sync-dist-repo.sh --zips <zip_dir> --tag /tmp/ra-swift\n# verify BEFORE pushing (content check passes locally; the tag check needs the push)\nRUNANYWHERE_SWIFT_DIST_REPO=/tmp/ra-swift \\\n    bash scripts/validation/gates/check_swift_dist_repo_sync.sh\ngit -C /tmp/ra-swift push origin main --follow-tags\nbash scripts/validation/gates/check_swift_dist_repo_sync.sh    # now [OK]\n```\n\nGotchas found doing this on 0.20.25:\n\n- `--check` **cannot be combined** with `--zips`/`--commit`/`--tag` (hard error). Run a\n  bare `--check <repo>` first to preview, then the real invocation separately.\n- `--check` legitimately reports `ERROR: distribution repo is out of sync` before you\n  sync — that's the preview telling you what it will change (expect the `sdkVersion` bump,\n  ~3 changed files, `0 added / 0 removed`). Non-zero exit there is not a failure.\n- The script **never pushes**; `--tag` only creates the tag locally. The dist repo tag has\n  **no `v` prefix** (`0.20.25`, not `v0.20.25`) while the monorepo tag does.\n- Running the gate locally right after `--tag` still shows `[FAIL] ... has no <version>\n  tag` while simultaneously showing `[OK] ... checkout matches: all 6 checksums`. That is\n  expected: the content check reads your local checkout, the tag check queries the real\n  remote. It clears the moment you push.\n- It chains into `sync-checksums.sh`, so run it **after** step 3's checksums are committed,\n  or the dist manifest gets stale hashes.\n\n---\n\n## 10c. Windows + self-hosted-runner traps (read before debugging a red Windows job)\n\nThese all cost a full CI cycle. The unifying fact:\n\n> The self-hosted runner service executes as `NT AUTHORITY\\NETWORK SERVICE`, which has its\n> own PATH **and** its own NTFS permissions. Anything you verify over SSH on that box tells\n> you nothing about what the job can do. `native_win_arm64_qhexrt` already carries a long\n> comment about this — read it before inventing a fix, and keep it in sync with neurun's\n> equivalent lane, which runs on the *same machine*.\n\n**Environment**\n\n- **`bash: command not found`** — Git for Windows puts `Git\\cmd` (git.exe) on PATH but not\n  `Git\\bin` (bash.exe). `actions/checkout` still works because it calls git.exe directly,\n  which makes this look unrelated to runner setup.\n- **`pwsh` does not exist on that box** — Windows PowerShell 5.1 only. Use\n  `shell: powershell`.\n- **`gh` is not installed on that box, and must not be assumed anywhere.** A QHexRT\n  download died on `gh: command not found`. The fix is deliberately *not* to install it:\n  every fetch of a private release asset now goes through\n  `fetch_release_asset()` in `scripts/build/_release_asset.sh`, which uses `curl` + the\n  REST API. `curl` ships with Windows, macOS and every Linux image we use, so this is one\n  code path everywhere instead of a per-runner provisioning step. If you add a new\n  consumer of a private asset, source that helper — do not reach for `gh`.\n  Note the helper matches the asset name **exactly**: a prefix match picks the `.sha256`\n  sidecar (its name starts with the tarball's), and you get a 65-byte \"archive\" that fails\n  much later with a confusing error.\n- **Tool availability is the single most common failure class on this runner** — `bash`,\n  `python3`, `ninja`, `gh`, GNU `tar` and NDK `.exe` suffixes have each broken a lane\n  once. When a self-hosted job fails, check \"does this tool exist as spelled\" before\n  reading any logic.\n- **`ln -s` under Git-Bash COPIES the directory** unless `MSYS=winsymlinks:nativestrict`.\n  It does not fail, so the result looks like real content and whatever validates a\n  \"must be a symlink\" invariant rejects a payload that was actually fine. Never use shell\n  `ln -s` on this box — go through `scripts/build/_selection.py`.\n- **Windows selects with a JUNCTION, not a symlink, and that is deliberate.** The runner\n  service account cannot create symlinks (below), but a junction is the same kind of\n  reparse point to a directory and needs no privilege. Two traps when reading that code:\n  `Path.is_symlink()` returns **False** for a junction (`stat.S_ISLNK` is true only for\n  `IO_REPARSE_TAG_SYMLINK`) — use `_selection.is_selection()`; and a junction stores an\n  **absolute** t\n\n<!-- File: .agents/skills/sdk-test-starters/SKILL.md (Tokens: ~7370 | Category: anthropic-skill) -->\n---\nname: sdk-test-starters\ndescription: Run a REAL end-to-end inference smoke test (model download + load + generate, with visible output) on every RunAnywhere starter app — iOS, macOS/Swift, Android, Web, Electron — after a runanywhere-sdks release. Build success alone is never sufficient evidence.\n---\n# SDK Test Starters\n\nPost-release validation runbook for the five RunAnywhere SDK consumer apps. This\nskill exists because \"it built\" has been reported as \"tested\" before, and that is\nnot evidence of anything except that the compiler ran.\n\n## 0. The evidence bar (applies to every app, no exceptions)\n\nFor each app you must produce, and be able to state explicitly:\n\n1. **Version proof** — print/log the actual resolved SDK version from inside the\n   running app (a startup log line, a version screen, an `swift package show-dependencies`\n   / `./gradlew dependencies` / `npm ls @runanywhere/...` output, etc). Never assume\n   a version bump \"took\" just because you edited a manifest.\n2. **Real model acquired** — a real (small) model downloaded fresh, or confirmed\n   already cached, by the app itself (not a file you hand-placed unless that IS the\n   app's supported flow).\n3. **Load** — the model actually loads (watch for a load-succeeded log/state, not\n   just \"no crash\").\n4. **One real inference call** — LLM `generate()` is almost always the easiest\n   modality to drive; use it unless another modality is genuinely less work for\n   that specific app.\n5. **Visible output** — capture the actual generated text (or audio/image) and\n   quote it in your report. \"It responded\" is not enough — show what it said.\n6. **Logs reviewed** — read the console/log stream for errors and warnings and\n   explicitly explain any that appear. Do not silently drop a stack trace because\n   the app \"still produced output.\"\n\nReporting an app as tested from a build/compile success alone is a skill violation.\nIf you could not get past build, that is a BLOCKED report (see §8), not a PASS.\n\n## 1. Locating each app's real local checkout\n\nThis machine accumulates many differently-named copies of the same repos over time\n(worktrees, experiment clones, old branches, scratch dirs). **Never trust a\ndirectory name.** Before touching anything in a candidate directory:\n\n```bash\ngit remote get-url origin          # must be https://github.com/RunanywhereAI/<repo>.git\ngit status --short --branch        # what branch, what's dirty, is it someone else's WIP?\n```\n\nAs of this writing, the known-good local checkouts for this user are here (verify\nthis each time — do not hardcode it further than \"check this location first\"):\n\n```\n~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-ios       -> RunanywhereAI/runanywhere-ios.git\n~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-android   -> RunanywhereAI/runanywhere-android.git\n~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-web       -> RunanywhereAI/runanywhere-web.git\n~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-electron  -> RunanywhereAI/runanywhere-electron.git\n```\n\nThe macOS/Swift case has no separate consumer repo — see §4, it lives inside\n`~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks` (confirm with\n`git remote get-url origin` there too — this machine also has stray full-monorepo\nclones elsewhere, e.g. under `~/development/hard/ambient/`, that are NOT the one to\nuse unless you've verified it's the intended checkout).\n\nThese starter checkouts are commonly sitting on a feature branch, not `main` — e.g.\nat last check `runanywhere-ios` and `runanywhere-android` were on\n`add-gemma4-qwen3.6-qwen3.8-models` and `runanywhere-web` on `add-supertonic-3-tts`.\nThat is fine to test FROM, but say so in your report, and do not `git checkout` away\nfrom a branch that has uncommitted work you didn't create — if `git status --short`\nshows dirty files you don't recognize, stop and ask rather than stashing/resetting.\n\nIf genuinely unsure which directory is real, `git clone` a fresh one into the\nscratchpad rather than guessing — a wrong guess wastes far more time than a clone.\n\n## 2. Cross-repo facts this skill assumes\n\n- SDK version SoT: `core/VERSION` in `runanywhere-sdks`, bumped everywhere by\n  `scripts/release/sync-versions.sh <version>`.\n- Release publish is `.github/workflows/release.yml` (dispatch or `v*.*.*` tag).\n- **Never** treat a build success against `RUNANYWHERE_USE_LOCAL_NATIVES=1` (or any\n  monorepo-local staged artifact) as proof the *public* release works — see §4.\n- QHexRT/QNN material must never appear in a public artifact — release.yml\n  hard-fails on it. If anything you're testing surfaces a QHexRT asset in a public\n  package, stop and flag it, don't route around it.\n\n## 3. iOS — `runanywhere-ios`\n\nSDK pin lives in `Package.swift`:\n\n```swift\n.package(url: \"https://github.com/RunanywhereAI/runanywhere-swift.git\", from: \"0.20.19\")\n```\n\nThis only resolves once `runanywhere-swift` (the generated Swift-only SPM\ndistribution repo — see `~/.claude/skills/sdk-publish/SKILL.md` §4) is\nactually tagged at the target version. Steps:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-ios\n# bump the `from:` version in Package.swift to the target release, then:\nswift package update            # or open in Xcode and let it resolve\n./scripts/build_and_run_ios_sample.sh simulator \"iPhone 16 Pro\"\n```\n\n`./scripts/verify.sh` is the CI-equivalent gate: it resolves the package remotely\nand runs a full `xcodebuild` against the iOS Simulator — use it to prove the\nresolve is clean (it fails if `Package.resolved` ends up dirty, i.e. the committed\npin was stale). `./scripts/smoke.sh` is a fast, non-compiling grep-based check —\nit does NOT count as evidence for this skill's bar, only `build_and_run` +\nwatching real output does.\n\nDrive one real `generate()` call through the app UI (or the equivalent debug\naction if the sample exposes one) against a small downloaded model, then watch the\nruntime log stream for the version line, the model download/load, the generation,\nand any errors:\n\n```bash\nlog stream --predicate 'subsystem CONTAINS \"com.runanywhere\"' --info --debug\n```\n\nMost loggers use `com.runanywhere.RunAnywhereAI`; a few use plain\n`com.runanywhere`; the SDK logs under its own subsystem — the `CONTAINS` match\ncovers all of them. On a physical device use `idevicesyslog | grep \"com.runanywhere\"`\ninstead.\n\n## 4. macOS/Swift — no separate repo, use the in-tree example or rcli\n\nThere is no `runanywhere-macos` consumer repo. Two in-monorepo options, both inside\n`~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks`:\n\n- `bindings/swift/example/` — a minimal SwiftPM example app/executable.\n- `rcli/` — the C++ core's CLI, `rac_*` C ABI consumer.\n\n**The critical gotcha**: the repo's own `Package.swift` is fail-closed toward the\n*real published release* — it resolves the remote `binaryTargets` unless you\nexplicitly opt into locally staged XCFrameworks with\n`RUNANYWHERE_USE_LOCAL_NATIVES=1`. Every doc/script example in this repo sets that\nenv var (because local dev wants fast iteration against in-progress binaries) —\nbut that means copy-pasting those examples verbatim tests your LOCAL staged build,\nnot the actual public release artifact. To prove the published release works:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks/bindings/swift/example\n# DO NOT set RUNANYWHERE_USE_LOCAL_NATIVES — leave it unset so Package.swift\n# resolves the real remote release binaries for the target version.\nswift build\nswift run runanywhere-minimal \"Name three colours.\"\n```\n\nIf the example was previously built/run with the env var set, `swift build` may\nreuse a stale resolve — run `swift package reset` (or delete `.build/`) first if\nyou're not sure which mode the last resolve used.\n\nFor rcli instead (also validates the real published core):\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks\nrcli models download qwen3          # ~639MB, small enough for a quick smoke test\nrcli llm generate --model qwen3 \"Reply with exactly: RCLI WORKS\" --reasoning off\n```\n\n`rcli models list --all` shows the rest of the built-in catalog\n(`whisper-tiny`, `smolvlm2`, `piper`, …) if LLM generate isn't the right modality\nfor what you're verifying.\n\n## 5. Android — `runanywhere-android`\n\nVersion pin: `gradle/libs.versions.toml` has ONE shared catalog entry:\n\n```toml\nrunanywhere = \"0.20.19\"\n...\nrunanywhere-sdk      = { group = \"io.github.sanchitmonga22\", name = \"runanywhere-sdk\",              version.ref = \"runanywhere\" }\nrunanywhere-llamacpp = { group = \"io.github.sanchitmonga22\", name = \"runanywhere-llamacpp\",          version.ref = \"runanywhere\" }\nrunanywhere-onnx     = { group = \"io.github.sanchitmonga22\", name = \"runanywhere-onnx\",               version.ref = \"runanywhere\" }\nrunanywhere-qhexrt   = { group = \"io.github.sanchitmonga22\", name = \"runanywhere-qhexrt-android\",     version.ref = \"runanywhere\" }\n```\n\n**CRITICAL — do not bump this block as one unit.** `runanywhere-qhexrt-android` is\npermanently frozen at whatever version it was last published at before being\nexcluded from the public Maven train; by policy it can never move forward via\npublic Maven again. Verify the actual frozen version FRESH every time (do not\ntrust a remembered number — it doesn't change, but don't skip the check):\n\n```bash\ncurl -s https://repo1.maven.org/maven2/io/github/sanchitmonga22/runanywhere-qhexrt-android/maven-metadata.xml\n```\n\nThe other three artifacts (`runanywhere-sdk`, `-llamacpp`, `-onnx`) CAN and should\nmove to the new release version. This means the single `runanywhere` catalog ref\nmust be **split into two entries** — one at the new version for sdk/llamacpp/onnx,\none pinned to the frozen qhexrt version — not bumped as a block.\n\n**Before trusting a mixed pin works**: verify `RAC_PLUGIN_API_VERSION` (the\nplugin ABI version constant in the C++ core, exposed identically to Kotlin) is\nunchanged between the old public SDK version and the new one. If it changed, mixing\na newer sdk/llamacpp/onnx with the frozen qhexrt version is NOT safe — this is a\nhuman decision (needs either an ABI-compat shim or dropping qhexrt from this build),\nnot something to force through.\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-android\n./gradlew :app:assembleDebug\n./gradlew :app:installDebug        # installs to a connected emulator/device\nadb logcat | grep -i runanywhere   # or a more specific tag if the app uses one\n```\n\nLaunch the app, drive one real `generate()` call against a small downloaded model\n(the app's own model picker/download flow), confirm the resolved SDK version is\nlogged, and review logcat for errors before reporting a PASS.\n\n## 6. Web — `runanywhere-web`\n\n`package.json` has 4 `@runanywhere/*` deps pinned with a caret range:\n\n```json\n\"@runanywhere/proto-ts\":    \"^0.20.19\",\n\"@runanywhere/web\":         \"^0.20.19\",\n\"@runanywhere/web-llamacpp\":\"^0.20.19\",\n\"@runanywhere/web-onnx\":    \"^0.20.19\"\n```\n\nBump all 4 to the new version, then:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-web\nnpm install\nnpm run build\nnpm run dev      # vite --host localhost --port 3000 --strictPort\n```\n\nThe dev/preview server is what supplies COOP/COEP headers required for\n`SharedArrayBuffer` — WASM inference will silently fail (or refuse to init\nthreads) without them, so don't swap in a bare static file server.\n\n`~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks/bindings/web/AGENTS.md`\nhas the authoritative Validation section (its own heading is `## Validation`) —\nfull checklist:\n\n1. Fresh browser context.\n2. Example app served with COOP/COEP headers.\n3. Model download.\n4. Model load.\n5. Real browser inference for the target modality.\n6. Logs/screenshots reviewed.\n\nIts \"Required release gates\" (run before browser validation) from the same repo:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks/bindings/web\nnpm run typecheck && npm run lint && npm run test && npm run build\nnpm run test:browser\nnpm run test:browser:release\ncd example && npm run typecheck && npm run build\n```\n\nStatic gates passing is not the smoke test — you still need an actual browser\nwatching real inference happen against the starter app. Prefer a real browser\nautomation tool (`claude-in-chrome` MCP tools, or Playwright — the maintained\nsuites under `bindings/web/tests/browser/` already use `playwright.config.ts`)\nover curling the served page: WASM inference genuinely needs a browser JS engine.\nLoad the served starter app, download/load a small model, trigger generate, and\nread the actual rendered output text plus the browser console — do not infer\nsuccess from an HTTP 200 on the page load.\n\n## 7. Electron — `runanywhere-electron` (SPECIAL CASE)\n\n**Do not assume a fresh SDK publish is available for this app.** Electron's own\nnpm packages (`@runanywhere/electron`, `-llamacpp`, `-onnx`, `-sherpa`, `-qhexrt`,\n`-neurt`) are on a SEPARATE, manual-publish pipeline — NOT part of `release.yml`\n(grep confirms zero mentions of these package names there). This separation is a\n**confirmed, deliberate** repo-owner decision, not a gap. Check\n`~/.claude/skills/sdk-publish/SKILL.md` §6 before assuming anything moved.\nElectron is **no longer WIP on a branch** — the old \"in-progress on\n`smonga/electron_upgrade`\" note is gone from the root `AGENTS.md`/`CLAUDE.md`\nentirely as of this checkout; `bindings/electron/` is a fully merged SDK on `main`.\nAs of `fix/electron-cicd-staging-url-npu-ane`,\n`.github/workflows/electron-native-package.yml` builds every backend for real and\npackages every tarball — but still does not publish; verify against a live run\nrather than assuming build automation status either way.\n\nTest the starter against whatever version it is CURRENTLY pinned to unless a human\nhas explicitly confirmed a newer Electron publish is ready and intended:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-electron\ncat package.json | grep '@runanywhere/electron'   # confirm current pin before touching it\n```\n\n**`@runanywhere/electron-qhexrt` being public on npm is a CONFIRMED, settled\npolicy decision** (repo owner, 2026-08-20), not a contradiction to flag — it\ncontains real Hexagon NPU binaries (`libQnnHtpV81Skel.so`, `QnnHtp*.dll`,\n`rac_commons.dll`, `runanywhere_qhexrt.dll`) on purpose. The QHexRT-stays-private\npolicy enforced elsewhere in this repo family is specifically about the\nGitHub-Release-side asset set (Android etc.), a different channel. Only re-raise\nthis if you find evidence the policy changed.\n\nTwo layers to actually exercise, do not conflate them:\n\n**(a) Cross-platform Electron JS + macOS native addon** — buildable and testable\nlocally on this Mac right now:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-electron\nnpm install\nnpm run build   # or the app's documented dev/start script — check package.json scripts\nnpm start        # or equivalent — launch the actual Electron app\n```\n\nDrive one real `generate()` call in the running app, watch its console/log output\nfor the resolved SDK version, model download, load, and generation, and read any\nerrors rather than ignoring them.\n\n**(b) Genuine Windows-native behavior** — a macOS build proves NOTHING here\nregardless of how clean the cross-platform code looks; the native addon and any\nWindows-only DLLs must be exercised on real Windows.\n\nThe team keeps a **Windows ARM64 test machine reachable over SSH**. Its hostname,\nuser, key, and VPN/mesh node name are operator-specific and deliberately not\nrecorded in this repo — read them out of the operator's own `~/.ssh/config` (look\nfor the Windows/ARM64 host entry) and use that alias below wherever `$WIN_BOX`\nappears. If no such entry exists, the box is not configured for you: say so and\nask, rather than guessing at a hostname.\n\n```bash\nWIN_BOX=<the ssh alias from ~/.ssh/config>\n```\n\n**Always check reachability first, never just try SSH and retry on timeout.** The\nbox lives on a private mesh VPN and is frequently asleep, so start by asking the\nVPN client whether the node is up (e.g. `tailscale status`, matching on the host\nentry's `HostName`) rather than opening a connection.\n\nIf the node reports `offline, last seen ...`, the box is asleep/powered off.\n**A connection timeout here means \"device asleep,\" not \"config is wrong.\"** Tell\nthe human and ask them to wake it; do not loop retrying SSH. Only once the node\nshows a live/idle state, confirm with:\n\n```bash\nssh -o ConnectTimeout=8 \"$WIN_BOX\" \"whoami\"\n```\n\nThen, with the repo already cloned there:\n\n```bash\nssh \"$WIN_BOX\" \"cd <repo-path-on-that-box>; git pull; npm install; npm run build\"\n# then run whatever launches the Windows Electron build and drive a real generate call\nscp \"$WIN_BOX\":<remote-log-or-artifact-path> ./local-copy   # pull back evidence to inspect\n```\n\nDo not claim Windows-specific validation happened unless you actually obtained a\nshell on that box and ran something real there — a clean macOS build is not\nsubstitute evidence.\n\n**`node_modules/electron` on this box silently tracks whichever arch last ran `npm\ninstall`, not which Node launches the test.** The x64 (llamacpp/onnx/sherpa) and\narm64 (QHexRT) lanes share ONE `runanywhere-electron` checkout/`node_modules` tree.\nPrepending an x64 Node to `PATH` and running `npx playwright test` does NOT make\nElectron itself x64 — `require('electron')` always resolves the single installed\n`node_modules/electron/dist/electron.exe`, and whichever arch that binary actually\nis determines which `prebuilds/win32-<arch>/` directory the running app looks in,\nregardless of `scripts/use-sdk.mjs local`'s staging (which itself is arch-aware\nbased on the *launching* Node, so it can correctly report \"staged win32-x64\" while\nthe process that will actually run is arm64 — a silent mismatch). Verify with:\n`powershell -Command \"$p=[IO.File]::ReadAllBytes('...\\electron.exe'); $peOff=\n[BitConverter]::ToInt32($p,60); [BitConverter]::ToUInt16($p,$peOff+4)\"` — `43620`\n(`0xAA64`) is ARM64, `34404` (`0x8664`) is x64. If it's the wrong arch (e.g. a prior\nsession's `npm install` for the other lane silently overwrote it), a genuine re-test\nof the other lane needs a FULLY SEPARATE checkout/`node_modules` tree installed\nunder the matching-arch Node — don't assume PATH-prepending alone is sufficient, and\ndon't trust `use-sdk.mjs status`'s \"platform: win32-x64\" output as proof the running\nElectron process is actually x64.\n\n**NPU hardware is a scarce, single-owner resource — serialize, never parallelize.**\nThis box has ONE physical Hexagon NPU. `engines/qhexrt/qhexrt_session.cpp`'s\n`session_open()` has a load-bearing comment on this exact failure mode: the HTP\nexposes a small number of protection domains with a per-PD context ceiling, so a\n*second* concurrent model load fails with `RAC_ERROR_BACKEND_INIT_FAILED`\n(\"Backend initialization failed\") — a fast (tens-of-ms), generic-looking failure\nthat is easy to mistake for a real regression. Confirmed once tonight: two agents\neach independently exercising QHexRT on this box at the same time produced exactly\nthis failure. Before concluding a QHexRT test genuinely failed, confirm nothing\nelse (another agent, a leftover Electron process, a stuck previous test run) is\nalso holding an NPU session open on this box — check for lingering `electron.exe`/\n`node.exe` processes from a prior run and kill them if orphaned, and never launch\ntwo NPU-touching verification passes concurrently on this machine.\n\n**\"Backend initialization failed\" has (at least) three distinct root causes —\ndon't stop at PD exhaustion.** Confirmed on 2026-08-17 after PD-exhaustion and\nstaleness were both ruled out (fresh rebuild, source-identical prebuilt, no other\nprocess holding a session): the *actual* bug was two compounding issues, both now\nfixed/documented —\n1. **Real source bug, fixed in PR #733**: `qhexrt_session.cpp`'s `runtime_acquire()`\n   called `qhx_runtime_create(nullptr, nullptr)` on every non-Android platform,\n   trusting the OS default DLL search order to find `QnnHtp.dll`/`QnnSystem.dll`.\n   On Windows that order never includes a dynamically-loaded sibling plugin's own\n   directory (`LOAD_WITH_ALTERED_SEARCH_PATH`, used to load `runanywhere_qhexrt.dll`\n   itself, only widens the search for *that* load call, not later `LoadLibrary`\n   calls made by code already running inside it) — so the QNN DLLs were invisible\n   even sitting right next to the plugin. Fixed by resolving the plugin's own\n   module directory (`GetModuleHandleExA`+`GetModuleFileNameA`) and passing\n   explicit paths, mirroring Android's existing `ADSP_LIBRARY_PATH` resolution in\n   the same file.\n2. **Operational staleness, NOT a script bug**: even after (1), the QNN runtime\n   DLLs actually staged into `@runanywhere/electron-qhexrt`'s\n   `prebuilds/win32-arm64` (`node_modules/.../electron-qhexrt/prebuilds/win32-arm64/`)\n   were from an old QAIRT build (~2.45.x vintage, Dec 2025 files) while the box's\n   installed QAIRT was 2.48.0.260626 — a real ABI-relevant version mismatch.\n   `bindings/electron/scripts/bundle-native.ts`'s `stageQnnRuntime()` just mirrors\n   whatever `RA_QNN_RUNTIME_DIR` points at with **no version check** — it's\n   correct-as-written; someone had pointed it at a stale neurun-side\n   `qairt-runtime-248`-named folder that was never refreshed. Fix: rebuild that\n   flat dir from the CURRENT QAIRT SDK's `lib/aarch64-windows-msvc/` (host DLLs:\n   `QnnHtp.dll`, `QnnHtpPrepare.dll`, `QnnHtpV81Stub.dll`,\n   `QnnHtpV81CalculatorStub.dll`, `QnnSystem.dll`) + `lib/hexagon-v81/unsigned/`\n   (DSP side: `libQnnHtpV81Skel.so`, `libqnnhtpv81.cat`), point\n   `RA_QNN_RUNTIME_DIR` at it, and re-run\n   `npm run bundle:native -- --package=qhexrt` from `bindings/electron/`.\n   **The already-published `@runanywhere/electron-qhexrt` npm package (as of\n   0.20.24) almost certainly ships the same stale DLLs** — this needs a\n   human decision (republish/patch) before claiming the published package works\n   on Windows NPU, separate from the source fix in PR #733.\n3. **PD exhaustion** (the pre-existing entry above) remains real and still applies\n   when two sessions genuinely contend for the one physical NPU — but it is not\n   the only explanation, and the log line to actually check first is the native\n   one: subscribe via `window.runanywhere.logging.records()` (after\n   `logging.setDebugMode(true)`) or add a temporary main-process log capture —\n   `app.process().stdout`/`stderr` do NOT carry it, because the addon runs in a\n   separate `utilityProcess.fork()` host, not the Electron main process, and\n   `RAC_LOG` is not an env var the logger reads (it does nothing). The generic\n   `[QHexRT] qhx_runtime_create failed (QNN libs unavailable?)` vs.\n   `qhx_model_load failed: ... N NPU model(s) already resident` log text is what\n   actually distinguishes cause (1)/(2) from cause (3) — always get that line\n   before diagnosing further.\n\n**Workflow gotcha:** if you orchestrate this via the Workflow tool, its agent()\ncalls do NOT reliably block-wait on a long-running remote build the way a direct\npoll loop does — observed once tonight where a workflow's own agents spawned an\ninternal Monitor/poller for the build log and then returned immediately,\nreporting \"still waiting,\" which let the whole workflow finish with nothing\nactually verified. For \"wait N tens-of-minutes for a real native build to\nfinish,\" use the top-level Monitor tool yourself (a shell poll loop over SSH\nchecking for a terminal BUILD_SUCCEEDED/BUILD_FAILED marker line) rather than\ndelegating the wait itself into a workflow agent's own judgment.\n\n### The Windows box's actual layout, and why it's more than a cross-platform check\n\n**This is a Hexagon-NPU-equipped Windows machine** (Snapdragon-class ARM Windows\ndevice, Copilot+ PC), not just a generic x86 Windows box. That makes it the one\nplace a QHexRT/Hexagon backend build+test can produce real device-only truth for\nWindows — matching the parent workspace's own \"x86 is a simulator, device-only\ntruth\" rule (see the top-level `Qualcomm/CLAUDE.md` and `QHexRT/CLAUDE.md`). Don't\ntreat this box as merely a Windows Electron smoke-test target; it's the only real\nHexagon NPU test rig reachable for this platform.\n\nPer the user (2026-08-17), under `~/Downloads/RunAnywhere/` on that box there are\nthree sibling repo checkouts at the same level — **verify each on connect, don't\nassume paths without checking, since this is relayed secondhand and may have\ndrifted**:\n- `runanywhere-sdks` — this monorepo, presumably already cloned; `git fetch && git\n  log -1 origin/main` to see how far behind it is before doing anything.\n- `runanywhere-electron` — the Electron starter app (same repo as\n  `RunanywhereAI/runanywhere-electron` elsewhere in this skill).\n- a third repo referred to verbally as \"the neuron/new-run inference engine for the\n  Hexagon NPU\" — almost certainly **`neurun`**, the QHexRT/NeuRT split-out repo\n  (see memory: \"neurun component split merged — QHexRT/ + NeuRT/ + shared/, root is\n  a closed 20-entry set enforced by a test\"). Confirm the actual directory name and\n  remote (`git remote get-url origin`) on connect rather than guessing from the\n  transcription.\n\n**Full task on this box (per explicit user instruction, not just a smoke test):**\n1. `git pull` (or fetch+fast-forward) all three repos to latest `main`.\n2. Rebuild the QHexRT/Hexagon NPU backend from `neurun` against this device's real\n   Hexagon NPU — this is the one environment where that build+run is meaningful\n   proof, not a simulator claim.\n3. Bump Electron's native dependencies to their latest versions — llama.cpp,\n   sherpa-onnx, ONNX Runtime, and whatever else `bindings/electron` pins — across\n   the repos involved. Treat this as a real dependency-bump task (check for\n   breaking API changes, don't just edit a version string and assume it builds).\n4. Update the model catalog for **all 5 starter apps across all 4 consumer repos**\n   (iOS, Android, Web, Electron — plus whatever the \"5th app\" split means for\n   macOS/Swift, see the rest of this skill) so the catalog is consistent everywhere,\n   not just on this Windows box.\n5. Run a real end-to-end inference pass on this box specifically (not just build\n   success) — both the general Electron/llama.cpp path AND the Hexagon NPU path via\n   `neurun`/QHexRT.\n6. Open PRs for whatever changed, in each affected repo. Do not merge them —\n   the user explicitly said they'll review.\n\nThis is real engineering work (native dependency bumps + an NPU backend rebuild),\nnot a mechanical version-string edit — expect it to take real time and to\ngenuinely need `QHexRT/CLAUDE.md` and `neurun`'s own docs read in full before\ntouching that backend, per the parent workspace's own instructions.\n\n## 8. Reporting\n\nFor each of the 5 apps (iOS, macOS/Swift, Android, Web, Electron), state exactly\none of these two outcomes — never blur them:\n\n- **FULLY VERIFIED** — with concrete evidence: the version string actually\n  resolved/logged, the model used, and the actual output produced (quote it).\n- **BLOCKED** — with the exact concrete reason, e.g.: the Windows ARM64 box's\n  mesh-VPN node is offline; Android emulator unavailable in this environment; App Store /\n  notarization credential missing; `runanywhere-swift` not yet tagged at target\n  version so `swift package update` can't resolve; electron-qhexrt policy conflict\n  needs a human call before proceeding further.\n\nA clearly reported partial/blocked result is far more valuable than an optimistic\nsummary that overstates what was actually observed. If you hit the known Electron\nqhexrt npm-publish policy conflict (§7), call it out in the report even if it\nwasn't the thing blocking your specific test — it's a standing issue every release\nnight should re-surface until a human resolves it.\n\n## Credentials referenced but never printed by this skill\n\nThese exist and are checked for presence only — never print their values:\n\n- Maven Central: `mavenCentral.username` / `mavenCentral.password`,\n  `signing.gnupg.keyName` / `signing.gnupg.passphrase` in `~/.gradle/gradle.properties`.\n- GPG signing key `CC377A9928C7BB18` — check with\n  `gpg --list-secret-keys --keyid-format LONG`.\n- macOS notarization profile `runanywhere-notary` — check with\n  `xcrun notarytool history --keychain-profile runanywhere-notary`; codesign\n  identity `Developer ID Application: RunAnywhere, Inc` — check with\n  `security find-identity -v -p codesigning`.\n- npm — `npm whoami` should already show a logged-in user.\n\nNone of the app-level smoke tests in this skill require touching these directly;\nthey matter only if a test path requires a fresh distro-repo publish (e.g.\n`runanywhere-swift` not yet tagged) — that's a separate concern of the\n`sdk-publish` skill, not this one.\n\n<!-- File: .agents/skills/telemetry-e2e/SKILL.md (Tokens: ~5937 | Category: anthropic-skill) -->\n---\nname: telemetry-e2e\ndescription: Prove that what an SDK binding EMITS on the telemetry wire is correct and that it actually arrives — vocabulary compliance, presence/absence semantics, device-id stability, and a received request body / stored row as evidence. Use when touching telemetry_json.cpp, telemetry_manager.cpp, rac_telemetry_vocabulary.h, any binding's platform-adapter telemetry bridge, or when asked to verify a telemetry field isn't silently dropping.\n---\n\n# Telemetry Wire Correctness (SDK side)\n\nThis is the SDK-side counterpart to the backend's `telemetry-e2e` skill (private\n`Runanywhere-monorepo` repo — proves event-to-stored-row on the server side). This one\nproves the other half: that commons *serializes* a call correctly and that the bytes\n*leaving* a binding are the bytes a real backend accepts. It does not cover per-starter-app\ninference smoke tests — see `sdk-test-starters` for that.\n\n## 0. The evidence bar\n\nCompiling is not evidence. A green `telemetry_extraction_tests` run is not evidence that a\nreal binding's real call site reaches the wire correctly — it only proves the *serializer*\nhandles a hand-built payload correctly. In order of increasing strength:\n\n1. **Unit test green** (`ctest -R telemetry_extraction_tests`) — proves the JSON builder's\n   presence/vocabulary rules are still correct in isolation. Necessary, not sufficient.\n2. **Received request body** — capture the actual JSON `rcli` (or a real binding) put on\n   the wire and read it: right modality endpoint, right keys, no vocabulary values that\n   got through that shouldn't have, measured zeros kept as `0` not `null`.\n3. **Stored row matches, field-for-field** — the backend's own ingest response\n   (`events_received`/`events_stored`/`events_skipped`) or a direct DB read shows the row\n   landed with the values you actually sent, not just a 2xx. This is the only tier that\n   also proves the vocabulary/schema on both sides still agree.\n\nA PR description that says \"sent telemetry, no errors\" without quoting a request body or a\nstored-row count is not evidence of anything except that a socket didn't error out.\n\n## 1. The emit path\n\nEvery binding funnels through the same two C ABI entry points in\n`core/src/infrastructure/telemetry/telemetry_manager.cpp`:\n\n- `rac_telemetry_manager_track` — flat-struct payload (`rac_telemetry_payload_t`), used by\n  `rcli`'s `telemetry emit`/`telemetry blast` and by hand-built events.\n- `rac_telemetry_manager_track_proto` — the real path every language binding actually uses:\n  a binding builds a protobuf `SDKEvent` (`sdk_events.pb.h`) at the real call site (e.g. an\n  LLM `generate()` completing), and `proto_event_type_string()` + the per-domain extraction\n  blocks in `telemetry_manager.cpp` turn it into the same flat payload. `framework_proto_to_string()`\n  / `framework_to_string()` / `clean_framework()` do the backend-name normalization here —\n  this is where a raw enum like `INFERENCE_FRAMEWORK_LLAMA_CPP` becomes the wire string `\"llamacpp\"`.\n- `stamp_live_device_state()` (same file) then overwrites the SDK-origin/device-state fields\n  (`sdk_binding`, `platform`, `battery_state`, memory, core count) from manager-owned state,\n  not from whatever the caller passed — a binding cannot inject a platform or binding value.\n- The payload is handed to `rac_telemetry_manager_payload_to_json()` in\n  `core/src/infrastructure/telemetry/telemetry_json.cpp`, which does field-by-field\n  serialization (see §2/§3 for the two contracts enforced there).\n- `send_batch_json()` batches by modality and POSTs to `/api/v2/sdk/telemetry/{modality}`\n  through whichever `rac_http_transport_ops_t` the binding registered (URLSession/OkHttp/\n  `emscripten_fetch`/curl for `rcli` — see the root `AGENTS.md`'s \"HTTP is platform-provided\"\n  rule). There is no `libcurl` fallback baked into commons itself.\n\nIf a field is going missing on the wire, the fix is almost always in\n`telemetry_json.cpp` (a field never wired into the serializer) or in the call site's proto\npopulation (a field never set on the `SDKEvent`) — not in a binding's bridge code, which\njust forwards bytes.\n\n## 2. The closed vocabularies — and the silent-drop rule\n\n`core/include/rac/infrastructure/telemetry/rac_telemetry_vocabulary.h` is a **generated\nfile** (see §5) holding six closed tables, each with a `RAC_TELEMETRY_<NAME>_COUNT` and a\n`RAC_TELEMETRY_<NAME>_VALUES[]` array:\n\n| Table | Count | Wire field | Values |\n|---|---|---|---|\n| `RAC_TELEMETRY_EVENT_TYPE` | 106 | `event_type` | namespaced strings, e.g. `llm.generation.completed`, `auth.succeeded`, `stt.transcription.completed` |\n| `RAC_TELEMETRY_BACKEND` | 22 | `backend` / `framework` | `onnx, sherpa, llamacpp, foundation_models, system_tts, fluid_audio, coreml, whisperkit_coreml, mlx, qhexrt, neurt, tflite, executorch, mediapipe, mlc, pico_llm, piper_tts, swift_transformers, cloud, builtin, none, unknown` |\n| `RAC_TELEMETRY_MODEL_SOURCE` | 3 | `model_source` | `catalog, builtin, user` |\n| `RAC_TELEMETRY_PLATFORM` | 9 | `platform` | `ios, android, macos, windows, linux, web, tvos, watchos, visionos` |\n| `RAC_TELEMETRY_SDK_BINDING` | 9 | `sdk_binding` | `swift, kotlin, flutter, react-native, web, electron, python, cli, cpp` |\n| `RAC_TELEMETRY_BATTERY_STATE` | 4 | `battery_state` | `charging, full, unplugged, unknown` |\n\n**The rule, verbatim from `add_vocabulary_string()` in `telemetry_json.cpp`:** if a value is\nnon-null but not present in its table, the field is dropped to `null` and the *rest of the\nevent still ships*. This is by design (a bad `backend` string must not cost the whole\nobservation) but it means:\n\n- The backend never sees the bad value, never 422s, never quarantines anything — there is\n  no error path anywhere for this. A field just isn't there.\n- The only client-side signal is a `RAC_LOG_WARNING(\"Telemetry\", \"Dropping %s: '%s' is not\n  in the published vocabulary\", ...)` log line — easy to miss unless you're specifically\n  watching debug logs.\n- `platform`, `sdk_binding`, and `battery_state` are stamped by the manager itself (§1), not\n  supplied by a binding's call site, so in practice only `backend`/`framework` (from the\n  engine/model layer) and `event_type` (only reachable via the proto path, checked at CI\n  time — see §5) are realistic places a new value can silently disappear.\n- `test_telemetry_extraction.cpp`'s vocabulary block (`\"vocab-1\"`/`\"vocab-2\"` cases) is the\n  concrete regression guard: it sends `framework = \"LlamaCpp\"` (a real spelling that used to\n  reach production) and asserts the string never appears on the wire while `model_id` still\n  does. If you touch `add_vocabulary_string()` or add a new vocabulary-gated field, extend\n  that test rather than trusting a manual check.\n\nHistorical motivation (from the header's own comment): before this table existed, stored\nproduction rows had four spellings of `llama.cpp`, three of ONNX, and binding names like\n`\"react-native\"` recorded as a `platform` — this file exists specifically to make that class\nof drift fail in this repo instead of being discovered in a database nobody was watching.\n\n## 3. Presence vs. absence: the \"measured zero\" contract\n\nBefore the fix documented in `telemetry_json.cpp`'s header, extractors read proto3 scalar\nfields directly — which report `0` for a field that was never set — and the old serializer\n(`add_int`/`add_double`) *skipped* zero values to keep payloads small. Result: a genuinely\nmeasured `output_tokens = 0` (an empty completion) and an unmeasured `tokens_per_second`\nboth arrived on the wire as absent, indistinguishable from each other.\n\nThe fix is `has_x`-gated serialization. Two helper families in `telemetry_json.cpp`:\n\n- `add_int_or_null(key, value, is_valid)` / `add_double_or_null(key, value, is_valid)` —\n  emit the real value (including `0`) when `is_valid` is true, else the JSON literal `null`.\n  Non-finite doubles (`NaN`/`Inf`) always degrade to `null` regardless of `is_valid`, because\n  JSON has no such literal and one bad value must not corrupt the whole batch.\n- The `is_valid` argument is a real `has_x`/sentinel check per field, not a blanket flag —\n  e.g. `payload->has_processing_time_ms != RAC_FALSE`, `payload->total_memory > 0`,\n  `payload->battery_level >= 0`.\n\n**What to check to confirm this stayed fixed** (this is exactly what\n`test_telemetry_extraction.cpp`'s presence block does — see §4): send an LLM completion\nevent with `output_tokens = 0` and `input_tokens = 0` explicitly set, and\n`tokens_per_second`/`context_length` left unset. Assert the wire body contains\n`\"output_tokens\":0` and `\"input_tokens\":0` as literal numbers, and `\"tokens_per_second\":null`\n/ `\"context_length\":null` — not all four as `null`, and not all four as `0`. If a future\nrefactor reverts to plain `add_int`/`add_double` for any of these fields, that regression\ntest is the thing that catches it; grep `telemetry_json.cpp` for `add_int(` / `add_double(`\n(the non-`_or_null`/non-`_always` forms) if you suspect a field regressed — those two still\nsilently skip zero by design and must never be used for a field where `0` is meaningful.\n\n## 4. Build, test, and what each telemetry test asserts (macOS)\n\n```bash\ncmake --preset macos-debug\ncmake --build build/macos-debug\nctest --preset macos-debug --output-on-failure -R 'telemetry|device_identity'\n```\n\nOr the non-preset form (`core/AGENTS.md`'s documented path):\n\n```bash\ncmake -B build -DRAC_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Debug\ncmake --build build\nctest --test-dir build --output-on-failure\n```\n\n`telemetry_extraction_tests` and `device_identity_tests` are registered in\n`core/tests/CMakeLists.txt`; `rcli_telemetry_live_tests` is registered separately in\n`rcli/tests/CMakeLists.txt`. All three are hand-rolled assertion runners — `CHECK()` in\nsome files, `ASSERT_EQ`/`ASSERT_TRUE` (from `core/tests/test_common.h`) in others — never\nGoogleTest:\n\n| ctest name | Binary | What it asserts |\n|---|---|---|\n| `telemetry_extraction_tests` | `test_telemetry_extraction` | Full proto→JSON pipeline per modality (LLM token counts/`tokens_per_second`, STT NaN-confidence never leaks as the string `\"nan\"`, TTS `characters_per_second`, embeddings `total_tokens`/`batch_size`/`embedding_dimension`, LoRA failure-path fields, RAG retrieval counts, VLM image/timing fields, live device-state stamping); the presence/measured-zero contract (§3); the closed-vocabulary drop rule (§2); and an HTTP-retry backoff (a failed batch is re-sent only after >5s, never before). Requires `RAC_HAVE_PROTOBUF` — the test binary itself no-ops with exit 0 if commons wasn't built with protobuf support. |\n| `device_identity_tests` | `test_device_identity` | Every branch of `rac_device_get_or_create_persistent_id()`'s resolution chain (§6) against a mocked platform adapter — cache hit, vendor-ID fallback, fresh-UUID generation, and that a `secure_set` failure doesn't silently drop the ID. |\n| `rcli_telemetry_live_tests` | `test_rcli_telemetry_live` | **Opt-in, always registered but a no-op by default.** Runs only with `--live` *and* `RUNANYWHERE_BASE_URL`/`RUNANYWHERE_API_KEY` set; otherwise prints \"skip\" and exits 0 — a green ctest run for this target proves nothing by itself. When live, it runs `rcli::bootstrap()` (real API-key → device-register → JWT handshake), sends one real event per modality (LLM, embeddings, RAG, VLM, LoRA-failure) through the real HTTP transport, and fails on any non-2xx response — this is the test that would catch a strict-schema `422 extra_forbidden` from field drift against the real V2 endpoints. |\n\nRun it live (§7 has the full local-backend flow):\n\n```bash\nRUNANYWHERE_BASE_URL=https://<backend-origin> RUNANYWHERE_API_KEY=<key> \\\n  ./build/macos-debug/rcli/tests/test_rcli_telemetry_live --live\n```\n\n`test_platform_bridge` (same `CMakeLists.txt`, adjacent target) is the wider companion\ncheck — \"every value a binding supplies must reach the wire\" — covering the platform half\nof the same two flat C structs (`rac_client_info_t`/`rac_device_registration_info_t`) for\nevery SDK at once; run it alongside if you're touching client-info fields rather than\ntelemetry event fields specifically.\n\n## 5. Regenerating the vocabulary header, and the drift gate\n\n`rac_telemetry_vocabulary.h` (§2) is generated from `idl/http/sdk-openapi.json` — the\ndevice-facing OpenAPI subset copied in from the private backend repo. **How that file gets\nupdated on the backend side, and how it crosses into this repo, is the monorepo's\n`openapi-contract` skill's job — read that by name in the other repo rather than\nre-deriving the steps here.** From inside this repo, once `idl/http/sdk-openapi.json` is\ncurrent:\n\n```bash\npython3 idl/codegen/generate_telemetry_vocabulary.py          # regenerate the header\npython3 idl/codegen/generate_telemetry_vocabulary.py --check  # CI drift check; exit 1 if stale\n```\n\n`.github/workflows/telemetry-vocabulary-drift.yml` runs three checks on every push to\n`main`/`development` and on any PR touching `idl/http/**`, the codegen script, or the\ntelemetry source/header dirs:\n\n1. **Generated header is current** — the `--check` command above.\n2. **Every event type the core can emit is in the vocabulary** — a Python check that parses\n   `proto_event_type_string()`'s body out of `telemetry_manager.cpp` via regex, unions in\n   the two prefix-expansion families (`stt.model.*`/`tts.voice.*`/`llm.model.*` and\n   `llm*`/`vlm*` generation suffixes), and diffs against `TelemetryEventType`'s enum in\n   `idl/http/sdk-openapi.json`. Anything emittable but unpublished fails the job.\n3. **Every framework string the core can emit is in the vocabulary** — same technique\n   against `framework_proto_to_string()` and `framework_to_string()` vs. `TelemetryBackend`.\n\n**A documented drift you will hit if you trust the wrong file**: `idl/http/README.md`'s own\n\"What it pins\" table names the schema for the `framework`/`backend` field as\n`TelemetryFramework` — that's stale. The actual generator\n(`idl/codegen/generate_telemetry_vocabulary.py`'s `VOCABULARIES` dict) and the header it\nproduces both say `TelemetryBackend`. Trust the generator and the generated header's own\n`// Published as X` comments over that README table.\n\n## 6. Per-binding device identity: where it's created, where it's persisted\n\nThe resolution chain itself is centralized in commons —\n`core/src/infrastructure/device/device_identity.cpp`\n(`rac_device_get_or_create_persistent_id`, tested by `device_identity_tests`, §4) — and is\nthe same fixed order for every binding:\n\n1. `adapter.secure_get(\"com.runanywhere.sdk.device.uuid\")` — if found and non-empty, use it,\n   full stop.\n2. `adapter.get_vendor_id` (if the platform adapter provides one) — use it, then persist it\n   via `secure_set` so step 1 hits next boot.\n3. Generate a fresh RFC-4122 v4 UUID, persist it via `secure_set`.\n\nWhat differs per binding is **only** the `secure_get`/`secure_set` backing store — an\nunstable backing store (one that doesn't survive a real app restart, or that resolves to a\ndifferent physical location than the read path expects) is exactly the bug class that\ninflates device counts, because step 3 fires again on every \"cache miss\":\n\n| Binding | Backing store | Where |\n|---|---|---|\n| Swift (iOS/macOS) | Keychain, service `com.runanywhere.sdk` | `bindings/swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+PlatformAdapter.swift` (`platformSecureGetCallback`/`platformSecureSetCallback`) |\n| Kotlin (Android) | Android Keystore AES-256-GCM key, ciphertext in a no-backup app file (not `SharedPreferences`) | `bindings/kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/security/AndroidKeychainManager.kt` (`NoBackupCiphertextStore`), wired through `CppBridgePlatformAdapter.PlatformSecureStorage` |\n| Flutter | Native Keychain/Keystore helper via FFI symbols `ra_flutter_secure_storage_{store,retrieve,delete}` | `bindings/flutter/packages/runanywhere/lib/native/dart_bridge_secure_storage.dart`; the device-UUID key constant is duplicated (deliberately, per its own comment) as `_keyDeviceUUID = 'com.runanywhere.sdk.device.uuid'` in `dart_bridge_device.dart` |\n| React Native (Android) | `SecureStorageManager` (native Android side) via `PlatformAdapterBridge.kt`'s `secureGet`/`secureSet` | `bindings/react-native/packages/core/android/src/main/java/com/margelo/nitro/runanywhere/PlatformAdapterBridge.kt` |\n| Web | `localStorage`, key `rac_sdk_plaintext_com.runanywhere.sdk.device.uuid` — **plaintext**, no OS-level encryption equivalent exists in a browser; restricted by convention to non-sensitive IDs | `bindings/web/packages/core/src/runtime/PlatformAdapter.ts` (`registerSecureGet`/`registerSecureSet`); also has a separate `stableVendorId()` fallback under key `rac_sdk_plaintext_vendor_id` |\n| Electron (macOS/Linux) | Flat file under a secure directory, path-traversal-guarded (`fs::path(key)` must not be absolute or contain `..`) | `bindings/electron/native/posix_platform_adapter.cpp` (`posix_secure_get`/`posix_secure_set`) |\n| Electron (Windows) | Windows DPAPI (`CryptProtectData`, current-user scope, no UI) | `bindings/electron/native/win32_platform_adapter.cpp` (`win_secure_get`/`win_secure_set`) |\n\nThe key string itself — `com.runanywhere.sdk.device.uuid` — is intentionally the same\nliteral across Swift, Flutter, and RN (each source comments cross-reference the other two by\nname), so grepping for it is a fast way to confirm a new binding is reading/writing the\n*same* key it thinks it is, not a typo'd sibling.\n\n**How to verify stability across a real restart** (not just \"the code looks right\"):\n\n1. Run the binding once, capture the emitted `device_id` from a real request body (§7) or\n   from `POST /api/v1/devices/register`'s response.\n2. Fully terminate the process (not just background it — on mobile this matters, a\n   backgrounded app can keep its in-memory device-id cache alive without ever touching\n   the read path again).\n3. Relaunch and repeat step 1.\n4. The `device_id` must be byte-identical. If it changed, the break is almost always one of:\n   the backing store not surviving process death (e.g. an in-memory-only stub used during\n   development), a key-name mismatch between the write and read paths, or (Android\n   specifically) `Context.deleteSharedPreferences` being invoked on the wrong file — see\n   `AndroidKeychainManager.kt`'s constructor, which deletes a hardcoded *legacy* prefs name\n   on every construction; if that name were ever widened to match the real store, every\n   Android launch would look like a fresh device.\n\nTwo-in-one launches on the *same physical device* (e.g. two emulator instances, or a\nsame-device reinstall vs. a real fresh install) are a different, real-world failure mode\nfrom a code bug — don't conflate an intentional reinstall's fresh ID with instability.\n\n## 7. Point a local build at a local backend and prove an event landed\n\n`rcli` is the fastest way to exercise the full wire path without a mobile/browser\ntoolchain — it links `rac_commons` directly and speaks real HTTP via curl.\n\n```bash\ncmake --preset rcli-macos-release   # or rcli-linux-release on Linux\ncmake --build build/rcli-macos-release --target rcli -j \"$(sysctl -n hw.logicalcpu)\"\n```\n\n**Keyless (development environment) — no API key, works against any backend that allows\nanonymous V2 telemetry:**\n\n```bash\n./build/rcli-macos-release/rcli/rcli --environment development \\\n  --base-url http://127.0.0.1:8000 \\\n  telemetry blast --processing-ms 42.5 --session-id my-check \\\n  --input-tokens 128 --output-tokens 256\n```\n\nThis is exactly the shape `scripts/ci/oss_keyless_telemetry_blast.sh` runs in CI against the\npublic staging backend — `--environment development --base-url <origin> telemetry blast\n...` with no `--api-key`. `telemetry blast` sends one representative event per modality (all\n12) and prints a `MODALITY | RESULT | STATUS | RECEIVED | STORED | SKIPPED` table parsed\ndirectly from the backend's own batch response — **the `STORED` column is authoritative\nserver-side confirmation**, not just an HTTP 2xx. `run_telemetry_blast()`'s own exit-code\nlogic (`rcli/src/commands/cmd_telemetry.cpp`) does fold `stored >= count` into its overall\npass/fail, so a genuine storage shortfall on any modality does make the process exit\nnon-zero, not just a transport error — but the exit code is one bit for all 12 modalities\ncombined, so a non-zero exit still requires reading the table to know *which* modality\nfailed and *why* (a `network error` status is a transport problem; an `HTTP 200` row with\n`STORED` under `RECEIVED` is a row-level rejection, e.g. a vocabulary value the backend\ndidn't recognize — a real bug to chase, not the same failure class at all).\n\n**Authenticated (production shape) — for exercising the login handshake too:**\n\n```bash\n./build/rcli-macos-release/rcli/rcli --environment production \\\n  --base-url https://<backend-origin> --api-key <key> \\\n  auth login\n./build/rcli-macos-release/rcli/rcli --environment production \\\n  --base-url https://<backend-origin> --api-key <key> \\\n  telemetry emit --modality llm --count 3 --input-tokens 128 --output-tokens 256\n```\n\n`auth login` only has a path in `production` — `development`'s keyless mode has no login\nstep by design (see `rcli/src/commands/cmd_auth.cpp`'s own subcommand help text).\n\n**To inspect the actual request body**, not just the summary table, add `--json` for\nmachine-readable CLI output and `-v` for debug logging (the debug log includes the\nserialized JSON per batch before it goes on the wire) — or point `--base-url` at a throwaway\nlocal capture endpoint (any script that logs the raw POST body) before pointing it at a real\nbackend, to sanity-check field shape offline first.\n\n**If you have the private backend repo checked out locally too**, running it yourself\n(`cd backend && uv run uvicorn main:app --reload`, per that repo's own README) and pointing\n`--base-url` at `http://127.0.0.1:8000` gets you the strongest possible evidence: query the\nSQLite/Postgres row directly after a blast and diff it field-for-field against what you sent\n— the monorepo's own `telemetry-e2e` skill (its integration suite, hermetic pytest fixtures,\nand `test_telemetry_conformance.py`'s vocabulary/presence/identity assertions against stored\nrows) is the authoritative reference for that side; don't re-implement its DB assertions\nhere, just point at it.\n\n## Evidence bar, restated\n\n- Compiling `rcli` or a binding: **not evidence**.\n- `ctest -R telemetry_extraction_tests` green: **not evidence of a real call site** — only\n  of the serializer in isolation.\n- `rcli telemetry blast` exiting 0: real signal (its exit code does fold in the backend's\n  `stored >= count` check, §7) but still worth pairing with the per-modality\n  `STORED`/`SKIPPED` table in your report — the exit code can't tell you *which* modality\n  or *why* a shortfall happened.\n- A captured request body showing the right endpoint, the right keys, correct\n  presence/absence per §3, and no off-vocabulary values per §2 — **evidence of correct\n  emission**.\n- That request body's values matching a stored row (via the backend's own\n  `events_stored` count at minimum, a direct DB read at best) — **evidence of correct\n  end-to-end delivery**, the only tier worth reporting as \"telemetry works.\"\n\n<!-- File: .claude/skills/engine-plugin-authoring/SKILL.md (Tokens: ~1903 | Category: anthropic-skill) -->\n---\nname: engine-plugin-authoring\ndescription: Step-by-step recipe for adding a new engine plugin under runanywhere-sdks/engines/, or adding a modality (e.g. TTS) to an existing engine (llamacpp, sherpa, onnx, cloud, mlx, neurt, qhexrt). Use when the task is \"add engine X\", \"wire up backend Y\", or \"make engine Z also serve <modality>\".\n---\n\n# Engine plugin authoring (`engines/`)\n\nTwo related runbooks: adding a brand-new engine plugin, and adding a modality to one\nthat already exists. Read `engines/AGENTS.md` first for the concepts these steps\nassume (op-table vtable, manifest, the valid-engine checklist, the 4-file skeleton,\nthe engine↔runtime rule). This skill is the \"how\", that file is the \"what/why\".\n\n**Invariant that applies to both procedures:** never invent a new modality just to\nship an engine — fill an existing `rac_engine_vtable_t` slot. Adding a brand-new\n*primitive* (a new `RAC_PRIMITIVE_*` + vtable slot) is a commons ABI change, covered\nby `core/AGENTS.md` → \"Adding a new capability interface\", not an engine change.\n\n---\n\n## Procedure A — add a new engine\n\nMirror **sherpa** (multi-modality, standalone `.so`) or **cloud** (single modality,\nno runtime) as your template; both are the cleanest references for the 4-file\nskeleton described in `engines/AGENTS.md`.\n\n1. **Create `engines/<name>/`** with the 4-file skeleton: `rac_plugin_entry_<name>.cpp`,\n   `rac_backend_<name>_register.cpp` (skip it if there's no extra bring-up, like\n   `neurt`), `rac_static_register_<name>.cpp`, plus your impl files.\n2. **Write the manifest** in `rac_plugin_entry_<name>.cpp`:\n   - `name` = snake_case identity (the library/framework you wrap, or your own\n     codebase unit — NEVER a modality name; see the naming rule in\n     `engines/AGENTS.md`).\n   - `primitives[]` = what you serve; fill the matching vtable slots; leave every\n     other slot explicit NULL (the all-NULL tripwire depends on this).\n   - `runtimes[]` — only devices your compute actually depends on (NULL for\n     cloud/HTTP engines) — see THE RULE in `engines/AGENTS.md`.\n   - `formats[]` = the `RAC_MODEL_FORMAT_ID_*` values you accept (NULL if there's\n     no local model file).\n   - `priority`, `availability` (PUBLIC/PRIVATE), `package_owner`/`package_name`.\n3. **Implement the op-table(s)** honoring the MODEL LIFECYCLE:\n   `create → initialize → use → cleanup → destroy` (see `engines/AGENTS.md` for the\n   exact signatures). Use `RAC_DEFINE_CREATE_ADAPTER(primitive, name)`\n   (`core/include/rac/plugin/rac_plugin_entry.h`) when `create` is a plain forward\n   onto `rac_<primitive>_<name>_create` — sherpa uses it for STT/TTS/VAD. Engines\n   with richer create flows (llamacpp, onnx, neurt, qhexrt) hand-write it.\n4. **`capability_check`** when the engine is platform- or binary-gated: use the\n   shared 3-way helper `rac_engine_unavailable_capability(platform_supported,\n   backend_present)` from `engines/common/rac_engine_unavailable.h`. When the\n   backend can be entirely absent at build time (a private prebuilt archive, a\n   sibling private repo), emit the stub with `RAC_ENGINE_UNAVAILABLE_PLUGIN(name,\n   display, cap_fn)` from the same header — see qhexrt and neurt for the two-mode\n   (ROUTABLE vs STUB) pattern this produces.\n5. **Registration carriers:** `RAC_PLUGIN_ENTRY_DEF(<name>)` returning the vtable; a\n   `rac_backend_<name>_register()`/`_unregister()` pair if you have extra bring-up\n   (called directly by SDK bridges on dynamic-link hosts); a\n   `rac_static_register_<name>.cpp` shim using `RAC_STATIC_REGISTER_BACKEND(<name>)`\n   (routes through the register fn) or `RAC_STATIC_PLUGIN_REGISTER(<name>)` (calls\n   the entry directly) for iOS/WASM static hosts.\n6. **CMake:** call `rac_add_engine_plugin(<name> SOURCES … LINK_LIBRARIES …\n   AVAILABILITY … PACKAGE_OWNER … PACKAGE_NAME …)` (`cmake/plugins.cmake`) from\n   `engines/<name>/CMakeLists.txt`, fronted by your own\n   `option(RAC_BACKEND_<NAME> \"…\" <default>)` + `if(NOT RAC_BACKEND_<NAME>)\n   return() endif()` self-gate (each engine owns its own option — `engines/CMakeLists.txt`\n   just unconditionally `add_subdirectory()`s and lets the child decide). Add\n   `add_subdirectory(<name>)` there. Declare `primitives`/`runtimes`/`formats` ONLY\n   in the C manifest, never in CMake, so the build graph can't drift from what the\n   router routes.\n7. **Android:** if the SDK loads the engine via `System.loadLibrary` +\n   `nativeRegister`, add a JNI bridge with `RAC_DEFINE_ENGINE_JNI_BRIDGE(...)`\n   (standalone per-engine `.so` — onnx, llamacpp) or\n   `RAC_DEFINE_ENGINE_JNI_BRIDGE_NO_ONLOAD(...)` (folded into a host lib that\n   already owns `JNI_OnLoad`, e.g. cloud → `librunanywhere_jni.so`) from\n   `engines/common/rac_engine_jni_bridge.h`. The JVM class-path token in the macro\n   call must match the Kotlin `*Bridge` class byte-for-byte.\n8. **iOS/WASM static hosts:** make sure the host force-loads the plugin —\n   `rac_force_load(<app> PLUGINS <name>)` in the host's CMakeLists — so the\n   static-init Registrar TU survives linker stripping.\n9. **If this engine ships an Electron/npm carrier package**, register it in\n   `scripts/validation/gates/check_plugin_natives.py`'s `REQUIRED_OPS` dict (the\n   ops-table symbol name(s) your entry TU references only on the routable\n   branch) — `package-sdk.sh` calls this automatically, so an unregistered\n   engine's stub/shell builds ship undetected. **Check which FILE actually\n   compiles `rac_plugin_entry_<name>.cpp`** before assuming the carrier\n   (`runanywhere_<name>`) is what to gate: sherpa/onnx/llamacpp compile it into\n   the carrier itself (an undefined symbol reference there is the evidence),\n   but neurt's `rac_add_engine_plugin()` call puts it on the ENGINE target\n   (`rac_backend_neurt`) instead — the carrier there is empty boilerplate with\n   zero symbols. Add the engine id to `EVIDENCE_IN_BACKEND_FILE` if it follows\n   neurt's shape; discovered by actually building neurt for real and running\n   `nm -a` on both files, not by reading the CMakeLists and assuming.\n\n## Procedure B — add a modality to an existing engine\n\nMulti-modality is the whole point of an identity-named engine. `cloud` and `neurt`\nare explicitly built for this and carry the recipe inline as a comment next to\ntheir vtable.\n\n1. Implement the new modality's op-table (e.g. a cloud TTS `g_cloud_tts_ops`,\n   typically backed by a per-modality provider adapter under `providers/`).\n2. **Fill the corresponding slot** in the engine's `rac_engine_vtable_t`\n   (`tts_ops` / `llm_ops` / `embedding_ops` / …) — every other slot must stay\n   explicit NULL.\n3. **Add the primitive** to the manifest's `primitives[]`.\n4. Update `formats[]`/`runtimes[]` only if the new modality actually needs them\n   (per THE RULE — don't declare a runtime your new op-table doesn't depend on).\n\nNo new plugin, no rename, no ABI bump — the engine already owns one\n`rac_engine_vtable_t` with all ten primitive slots; you're just filling another one.\nGrep the target engine's `rac_plugin_entry_<name>.cpp` for the phrase \"To add a\"\n— cloud and neurt both leave a comment naming exactly which slots are still free\nand what to add to `primitives[]`.\n\n<!-- File: .claude/skills/sdk-publish/SKILL.md (Tokens: ~10460 | Category: anthropic-skill) -->\n---\nname: sdk-publish\ndescription: Manually publish an already-tagged, already-released (non-draft GitHub Release) RunAnywhere SDK version to every package registry (npm, Maven Central, pub.dev, Swift SPM dist repo) plus macOS rcli notarization, with heavy verification that nothing fake/stubbed/broken ships.\n---\n\n# SDK Publish\n\nRepo: `~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks` (verify with `pwd`/`git remote get-url origin` — do not assume this path is still correct, checkouts move). Version source of truth: `core/VERSION`. Throughout, `$VERSION` means the bare semver being published (e.g. `0.20.24`), never prefixed with `v` except in git tags / GitHub Release names.\n\n## THIS PROCESS IS INTENTIONALLY MANUAL\n\n**`release.yml` never publishes anywhere public.** It only builds artifacts, packages them, and creates a draft/published **GitHub Release**. It does not call `npm publish`, does not upload to Maven Central, does not call `flutter pub publish`, and does not push the Swift dist repo. Verify this is still true before trusting it — greps below should all return **zero** matches (grep the whole file, not a snippet):\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks\ngrep -inE \"npm publish|publishReleasePublication|publishAllPublicationsToMavenCentral|flutter pub publish\" .github/workflows/release.yml\n```\n\nThis is a **deliberate safety gate**, not a gap. Every step below is a human (or an agent acting on a human's explicit go-ahead) pushing a button that CI intentionally never pushes. Do not propose wiring any of this into CI as a \"fix\" — that would remove the gate.\n\n## Precondition check (always run first)\n\nRefuse to proceed if the release is a draft or the asset set looks incomplete.\n\n```bash\ngh release view v$VERSION --json isDraft,tagName --jq '{isDraft, tagName}'\n# isDraft MUST be false.\n\ngh release view v$VERSION --json assets --jq '.assets[].name' | sort\n```\n\nCross-reference the listing against the `sdk-release` skill's step 8 asset-count expectations (`~/.claude/skills/sdk-release/SKILL.md`). As a second, independent check, also diff against the **previous** tagged release's asset list — same count, same name patterns, only the version token differs:\n\n```bash\ngh release view v<PREVIOUS_VERSION> --json assets --jq '.assets[].name' | sed \"s/<PREVIOUS_VERSION>/$VERSION/\" | sort >/tmp/expected.txt\ngh release view v$VERSION --json assets --jq '.assets[].name' | sort >/tmp/actual.txt\ndiff /tmp/expected.txt /tmp/actual.txt\n```\n\nAs of the last verified run (**v0.20.25, 65 assets** — up from 55 at v0.20.24 because the 5 public Electron tarballs and their sidecars joined the release; see §6), the full set is: `MANIFEST.txt`; per-platform `RACommons-{android-arm64-v8a,android-armeabi-v7a,android-x86_64,ios,linux-x86_64,web,windows-x64}-v$VERSION.{zip,tar.gz}` + `.sha256`; iOS backend zips `RABackend{LLAMACPP,MLX,NeuRT,ONNX,Sherpa}-ios-v$VERSION.zip` + `.sha256`; `RunAnywhereMLX{Metal,Resources,Runtime}-ios-v$VERSION.zip` + `.sha256`; `rcli-{linux-x86_64,macos-arm64,windows-x86_64}-v$VERSION.tar.gz`/`.zip` + `.sha256`; the 8 npm tarballs `runanywhere-{core,llamacpp,mlx,onnx,proto-ts,web,web-llamacpp,web-onnx}-$VERSION.tgz` + `.sha256`; the **5 Electron npm tarballs** `runanywhere-electron{,-llamacpp,-onnx,-sherpa,-neurt}-$VERSION.tgz` + `.sha256` (new as of v0.20.25); and `runanywhere-kotlin-maven-v$VERSION.zip` + `.sha256`. There is **no** Flutter tarball in the release (Flutter packages are staged from the native archives above, not from a dedicated release asset) and **no** `.dmg` asset in the baseline set (the macOS rcli tarball is what ships; see the notarization section for when a `.dmg` does get produced). If a `qhexrt`-named asset appears anywhere in this list, stop — that would mean `release.yml`'s own manifest assertion should have hard-failed and something is very wrong; do not publish anything from that release.\n\n## 0. MANDATORY GATE — verify the ENTIRE release before pushing anything to any registry\n\n**Run this once, in full, before the first registry push. Do not publish a single package until it passes.** Per-package spot-checks are not a substitute: they are how a bad artifact reaches a registry you cannot un-publish from. Proven end to end on v0.20.25 (2026-08-22), then independently audited and corrected: **34 payloads, 6619 files across 3 nesting levels, 282 binaries, 0 placeholder hits, 38/38 commons binaries carrying the real staging host**. The audit found 4 methodology gaps in the first pass (shallow nesting, missing `MTLB` magic, `.lib` excluded from the positive check, and a set-intersection that could pass vacuously) — every one is called out inline below, because each produced a *confident but under-covered* result.\n\n```bash\nVERSION=0.20.25                      # no leading v\nREPO=RunanywhereAI/runanywhere-sdks\nW=/tmp/relverify-$VERSION\nrm -rf \"$W\" && mkdir -p \"$W/assets\" \"$W/qhexrt\" \"$W/x\" \"$W/nested\"\n\n# (1) EVERY release asset — no -p filter, get the whole set.\ngh release download \"v$VERSION\" --repo \"$REPO\" -D \"$W/assets\"\n\n# (2) The PRIVATE QHexRT artifact is NOT a release asset — pull it from the run.\n#     Find the run that produced the release, then:\ngh run download <release-run-id> --repo \"$REPO\" --name sdk-electron-qhexrt-private --dir \"$W/qhexrt\"\n```\n\n### (3) Checksums — `cd` into the directory first\n\nSidecars contain a **bare basename**, so `shasum -c` fails with a misleading \"FAILED open or read\" if run from anywhere else. This will make every artifact look broken when nothing is wrong:\n\n```bash\ncd \"$W/assets\"\nok=0; bad=0\nfor s in *.sha256; do\n  if shasum -a 256 -c \"$s\" >/dev/null 2>&1; then ok=$((ok+1)); else echo \"MISMATCH: $s\"; bad=$((bad+1)); fi\ndone\necho \"verified=$ok mismatched=$bad\"      # bad MUST be 0\n(cd \"$W/qhexrt\" && shasum -a 256 -c ./*.sha256)\n```\n\n### (4) Extract everything — INCLUDING nested archives\n\n**The gotcha that matters:** `runanywhere-kotlin-maven-v*.zip` contains `.aar` files, and the `.aar`s are what hold the Android `.so` binaries. A one-level extraction reports \"0 binaries\" for the Maven bundle and every subsequent scan passes **vacuously**.\n\n**Recurse until the tree is dry — TWO levels are required here, not one.** Each `.aar` also contains a `classes.jar`, holding ~2027 `.class` files that are doubly unreachable: inside an unopened jar *and* excluded by the `.class` suffix rule most magic-byte scanners use. Level 2 adds **2033 files** (0 placeholder hits on v0.20.25); level 3 is empty. Loop until a pass finds no new archives rather than hardcoding a depth.\n\n```bash\ncd \"$W/assets\"\nfor f in *; do\n  case \"$f\" in *.sha256|MANIFEST.txt) continue;; esac\n  d=\"$W/x/$f\"; mkdir -p \"$d\"\n  case \"$f\" in\n    *.zip)              unzip -qq -o \"$f\" -d \"$d\" || echo \"EXTRACT FAILED: $f\" ;;\n    *.tar.gz|*.tgz)     tar -xzf \"$f\" -C \"$d\"     || echo \"EXTRACT FAILED: $f\" ;;\n    *) echo \"UNHANDLED TYPE: $f\" ;;\n  esac\ndone\nd=\"$W/x/$(basename \"$W\"/qhexrt/*.tgz)\"; mkdir -p \"$d\"; tar -xzf \"$W\"/qhexrt/*.tgz -C \"$d\"\n\n# nested pass\ncd \"$W/x\"\nfind . -type f \\( -name '*.aar' -o -name '*.jar' -o -name '*.zip' -o -name '*.tgz' -o -name '*.tar.gz' \\) \\\n  | sed 's|^\\./||' | while IFS= read -r a; do\n    dd=\"$W/nested/$(printf '%s' \"$a\" | tr '/' '_')\"; mkdir -p \"$dd\"\n    case \"$a\" in *.aar|*.jar|*.zip) unzip -qq -o \"$a\" -d \"$dd\";; *) tar -xzf \"$a\" -C \"$dd\";; esac\n  done\n```\n\n**Prove extraction actually happened** — an empty dir makes \"0 hits\" meaningless:\n\n```bash\nfor d in \"$W\"/x/*/; do\n  n=$(find \"$d\" -type f | wc -l)\n  [ \"$n\" -lt 2 ] && echo \"SUSPICIOUSLY EMPTY: $d ($n files)\"\ndone\n```\n\n### (5) Placeholder scan — EVERY file, not just binaries\n\nThe staging URL can also live in JS/JSON/TS config, so scan by content across all file types, and additionally scan the raw compressed streams in case a member never got extracted:\n\n```bash\nfind \"$W/x\" \"$W/nested\" -type f | wc -l                                     # total files covered\ngrep -rla \"YOUR_STAGING_BASE_URL\" \"$W/x\" \"$W/nested\" | tee /tmp/ph.txt | wc -l   # MUST be 0\nfor f in \"$W\"/assets/*.tgz \"$W\"/assets/*.tar.gz \"$W\"/qhexrt/*.tgz; do\n  gzip -dc \"$f\" 2>/dev/null | grep -qa \"YOUR_STAGING_BASE_URL\" && echo \"RAW HIT: $f\"\ndone\n```\n\nFind binaries by **magic bytes, not extension** (an unsuffixed framework binary like `RACommons.framework/RACommons` has no extension and a name-based `find` misses it). Signatures to cover:\n\n| Magic | Kind | v0.20.25 count |\n|---|---|---|\n| `7f454c46` | ELF | 177 |\n| `!<arch>` (`213c617263683e0a`) | static `.a` **and `.lib`** | 41 |\n| `MZ` | PE | 29 |\n| `cffaedfe` / `cefaedfe` | Mach-O | 19 |\n| `00 61 73 6d` | wasm | 10 |\n| **`4d544c42` (`MTLB`)** | **Metal library** — easy to miss | 5 |\n| `cafebabe` / `bebafeca` | fat Mach-O (exclude `.jar`/`.class`) | 1 |\n\n**Total: 282 binaries.** An earlier pass reported 277 because `MTLB` was absent from the signature list — `.metallib` files were invisible to the binary scan (they were still covered by the all-file `grep` in step 5, so the conclusion held, but the count was wrong). If your total comes in materially under ~282 for a comparable release, your detection is leaking.\n\n### (6) POSITIVE staging-URL check — absence of the placeholder is NOT sufficient\n\nA build with an **empty** URL baked in passes a placeholder-only check. `check_plugin_natives.py` only tests absence, so add the positive assertion yourself.\n\nTwo traps that produced false confidence on the v0.20.25 pass, both worth avoiding:\n\n- **Include `.lib`.** A `MUST_HAVE` regex of `rac_commons\\.(dylib|so|dll|a)$` silently skips\n  `RACommons-windows-x64/x64/rac_commons.lib` — an **89 MB** binary that then gets no positive\n  check at all. Match `(dylib|so|dll|a|lib)` plus `\\.wasm$`. Correct coverage is **38** binaries,\n  not 37.\n- **Assert per binary, not by set-intersection.** Intersecting \"non-public origins across all\n  files\" yields more than one origin here (`hf.co` *and* the staging host), so a binary carrying\n  only `hf.co` would pass **vacuously**. Check each binary individually for the staging host.\n\n```bash\n# v0.20.25 baseline: 38 commons/wasm/lib binaries; 0 missing the staging host.\n```\n\nNever print the URL — it is a repo secret. Report a truncated SHA-256 fingerprint, or just assert presence/absence counts.\n\n### (7) Routability gate over the whole tree\n\n```bash\npython3 scripts/validation/gates/check_plugin_natives.py \"$W/x\" \"$W/nested\" > /tmp/gate.txt 2>&1\ngrep -cE '^  ok ' /tmp/gate.txt; grep -E '^  FAIL' /tmp/gate.txt\n```\n\nv0.20.25 baseline: **77 ok, 1 FAIL**. The single expected FAIL is\n`RACommons-linux-x86_64/lib/librunanywhere_sherpa.so` — see the known-exception note below. **Any other FAIL blocks the publish.**\n\n### (8) Known exception — Linux sherpa is a non-routable shell (PRE-EXISTING, not a regression)\n\n`RACommons-linux-x86_64-v*.tar.gz` ships **no** `libsherpa-onnx-c-api.so` at all and its `librac_backend_sherpa.so` is a ~156 KB shell (~3 sherpa strings, vs ~31 MB / ~192 for a healthy one on Electron macOS) — the engine is not linked in, so the carrier cannot reference `g_sherpa_{stt,tts,vad}_ops` and **STT/TTS/VAD cannot work from that bundle**. Android's carriers pass on all 3 ABIs, so this is Linux-only.\n\n**Compare the CARRIER, not the backend.** The gate FAILs on `librunanywhere_sherpa.so` (the carrier); citing `librac_backend_sherpa.so` as your evidence is comparing a *different file* and is not proof. The carrier is byte-identical across releases, which is the real proof:\n\n```bash\ngh release download \"v<PREV>\" --repo \"$REPO\" -p 'RACommons-linux-x86_64-v<PREV>.tar.gz' -D /tmp/prev\ntar -xzf /tmp/prev/*.tar.gz -C /tmp/prev\nshasum -a 256 /tmp/prev/lib/librunanywhere_sherpa.so \"$W\"/x/RACommons-linux-*/lib/librunanywhere_sherpa.so\n# v0.20.24 and v0.20.25 both: size=15712  sha256=28ef16015b61da86...\n\n# strongest check — run the gate on the PREVIOUS bundle and confirm the identical FAIL:\npython3 scripts/validation/gates/check_plugin_natives.py /tmp/prev\n```\n\nIf it ever regresses *further* (e.g. Android or macOS sherpa also goes hollow), stop and treat it as a blocking defect.\n\n### (9) Account for every payload explicitly\n\nPrint a per-artifact table (checksum verdict, binary count, placeholder hits) and confirm the row count equals the payload count. Artifacts that legitimately contain **no** native binaries, and why — do not let these pass silently unexamined:\n\n| Artifact | Why it has no binaries |\n|---|---|\n| `RunAnywhereMLXResources-ios-*.zip` | 25 third-party license/notice text files |\n| `runanywhere-kotlin-maven-v*.zip` | binaries live in the **nested** `.aar`s (step 4) |\n| `runanywhere-proto-ts-*.tgz` | pure TS/JS (69 `.ts` + 69 `.js`) |\n\nEverything else must have a non-zero, plausible binary count. A platform suddenly shipping fewer binaries than the previous release is a regression signal worth chasing before publishing.\n\n### (10) Blind spot to be aware of — Windows x64 has NO routability coverage\n\n`RACommons-windows-x64-v*.zip` ships **only** a static `rac_commons.lib` (~89 MB) plus 152 `.h` / 11 `.hpp` headers — **zero backend DLLs**, where Linux/Android/Web each ship 4–5 backend carriers. That is consistent with a static-embedding bundle (consumers link the engines in themselves), but the consequence matters: **no gate can detect a missing or hollow Windows backend**, because there is no carrier to inspect. `check_plugin_natives.py` therefore reports nothing at all for this artifact rather than reporting health.\n\nWindows x64 is also *advisory* in `release.yml` (its absence does not fail the release). So a Windows regression can pass every automated check. If Windows correctness matters for a given release, verify it by actually running something on the real Windows box (see §Electron for how to reach it) — do not infer it from a green pipeline. Worth a deliberate confirmation with the repo owner that the DLL-free shape is intentional.\n\n---\n\n## General verification discipline (every artifact, every registry)\n\nApply all three checks below to **every** file you're about to publish, before AND after the publish call. Never trust a copy cached locally earlier in the session — re-download.\n\n**(a) Download + verify the sha256 sidecar yourself:**\n\n```bash\ngh release download v$VERSION -p '<file>' -p '<file>.sha256' -D /tmp/sdk-publish-$VERSION\ncd /tmp/sdk-publish-$VERSION && shasum -a 256 -c '<file>.sha256'\n```\n\n**(b) Inspect the actual file listing inside every tarball/AAR/zip** — never assume from the filename alone:\n\n```bash\nnpm pack <tgz-or-package-dir> --dry-run     # npm packages: prints the exact file list it would publish\nunzip -l  <file.zip>                        # AARs, Maven repo zip, native archives\ntar -tzf  <file.tar.gz>                     # rcli, RACommons tarballs\n```\n\nSanity-check what comes back:\n- No suspiciously-small or empty binaries where a real native library (`.so`, `.dylib`, `.a`, `.dll`) should be — a stub or a zero-byte placeholder means the build silently failed upstream.\n- No leftover `workspace:*` or `file:` dependency specs in any `package.json` inside the tarball — those only resolve inside this monorepo and break for a real external installer.\n- Version strings inside every manifest (`package.json` `version`, AAR `pom.xml` `<version>`, `pubspec.yaml` `version:`) **exactly** match `$VERSION`.\n- **Hard rule, no exceptions:** grep the full file listing for `qhexrt` (any case) and for QNN binary names (`libQnnHtp`, `QnnHtp*.dll`, `rac_commons.dll` bundled with QHexRT, `runanywhere_qhexrt`). If any hit appears in a package meant to be public, stop and do not publish that artifact.\n\n**(c) Confirm baked-in runtime config** (e.g. a staging-vs-prod backend base URL compiled into `rac_commons` at build time) points at the real intended endpoint. This is designed to fail closed if misconfigured — a build that completed and produces working requests against prod is itself decent evidence — but if you have any doubt, `strings <binary> | grep -i \"api\\.\\|staging\\.\"` and eyeball it.\n\n## 1. npm — 8 packages, order matters\n\nPackages: 4 Web (`@runanywhere/proto-ts`, `@runanywhere/web`, `@runanywhere/web-llamacpp`, `@runanywhere/web-onnx`) + 4 React Native (`@runanywhere/core`, `@runanywhere/llamacpp`, `@runanywhere/mlx`, `@runanywhere/onnx`). Never `@runanywhere/qhexrt` (RN) — it exists in `bindings/react-native/packages/qhexrt/` but is excluded from the public npm train by the same policy as Maven/pub.dev QHexRT.\n\n**Publish `@runanywhere/proto-ts` FIRST.** `@runanywhere/web-onnx` deliberately does not bundle its own `proto-ts` copy and needs it resolvable from the live registry for real consumers; publish `proto-ts` before it or a fresh `npm install @runanywhere/web-onnx` fails to resolve.\n\n```bash\ngh release download v$VERSION -p 'runanywhere-proto-ts-*.tgz*' -p 'runanywhere-web-*.tgz*' \\\n  -p 'runanywhere-core-*.tgz*' -p 'runanywhere-llamacpp-*.tgz*' -p 'runanywhere-mlx-*.tgz*' -p 'runanywhere-onnx-*.tgz*' \\\n  -D /tmp/sdk-publish-$VERSION\ncd /tmp/sdk-publish-$VERSION\nfor f in *.sha256; do shasum -a 256 -c \"$f\"; done\n\nnpm publish runanywhere-proto-ts-$VERSION.tgz --access public   # FIRST, always\nnpm publish runanywhere-web-$VERSION.tgz --access public\nnpm publish runanywhere-web-llamacpp-$VERSION.tgz --access public\nnpm publish runanywhere-web-onnx-$VERSION.tgz --access public\nnpm publish runanywhere-core-$VERSION.tgz --access public\nnpm publish runanywhere-llamacpp-$VERSION.tgz --access public\nnpm publish runanywhere-mlx-$VERSION.tgz --access public\nnpm publish runanywhere-onnx-$VERSION.tgz --access public\n```\n\n`npm whoami` should already show a logged-in account; if it errors, stop and tell the human rather than trying to log in non-interactively.\n\n**GOTCHA — large tarballs can silently die mid-upload.** `@runanywhere/mlx` (~50MB) and `@runanywhere/web-onnx` (unpacks to 300+MB) can exceed a short default shell/tool timeout mid-upload and get killed with no clean error. Use an explicit long timeout (5 minutes / 300000ms) on each publish call.\n\n**GOTCHA — verify via the registry API, not the npm CLI.** `npm view` can lag the CLI's own cache by several seconds right after a fresh publish and falsely report \"not found\". After **every** publish:\n\n```bash\ncurl -s \"https://registry.npmjs.org/@runanywhere/<pkg>\" | python3 -c \"import json,sys;print(json.load(sys.stdin)['dist-tags'])\"\n```\nConfirm `latest` equals `$VERSION`.\n\n## 2. Maven Central — 3 AARs only\n\n`io.github.sanchitmonga22:{runanywhere-sdk,runanywhere-llamacpp,runanywhere-onnx}`. **Never `runanywhere-qhexrt-android`** — it was published through 0.20.19 under an earlier, since-reversed policy and is now permanently excluded; `release.yml`'s manifest assertion hard-fails if it reappears in the public asset set.\n\nFull step-by-step lives in `bindings/kotlin/docs/KOTLIN_MAVEN_CENTRAL_PUBLISHING.md`, section **\"Option A: Publish with released native archives\"** (line ~108 as of this writing — the doc has been corrected mid-release before, so re-read it fresh rather than trusting memory of its contents). Key points, condensed:\n\n1. Download + verify the 3 `RACommons-android-{arm64-v8a,armeabi-v7a,x86_64}-v$VERSION.zip` from the release (sha256 sidecars, per the general discipline above).\n2. Stage: `bash bindings/kotlin/scripts/package-sdk.sh --mode local --natives-from <dir-with-the-3-zips>`. This also produces a local Maven-repo zip — diff its size against `runanywhere-kotlin-maven-v$VERSION.zip` already attached to the GitHub Release as a cheap reproducibility check.\n3. Before uploading, confirm the OSSRH compatibility service has no dangling open repository for this namespace:\n   ```bash\n   CENTRAL_BEARER=\"$(printf '%s:%s' \"$MAVEN_CENTRAL_USERNAME\" \"$MAVEN_CENTRAL_PASSWORD\" | base64 | tr -d '\\r\\n')\"\n   curl -s -H \"Authorization: Bearer $CENTRAL_BEARER\" \\\n     'https://ossrh-staging-api.central.sonatype.com/manual/search/repositories?ip=client&profile_id=io.github.sanchitmonga22'\n   ```\n   Resolve any stale repository per the doc's troubleshooting table before continuing.\n4. Source `MAVEN_CENTRAL_USERNAME`/`MAVEN_CENTRAL_PASSWORD` from `~/.gradle/gradle.properties` (`mavenCentral.username`/`mavenCentral.password` — confirmed present on this machine; never print the values) and run, from `bindings/kotlin/`:\n   ```bash\n   ./gradlew clean \\\n     :publishReleasePublicationToMavenCentralRepository \\\n     :modules:runanywhere-core-llamacpp:publishReleasePublicationToMavenCentralRepository \\\n     :modules:runanywhere-core-onnx:publishReleasePublicationToMavenCentralRepository \\\n     -Prunanywhere.useLocalNatives=true -x buildLocalJniLibs --no-daemon\n   ```\n   GPG signing (`signing.gnupg.keyName`/`signing.gnupg.passphrase` in the same properties file, key id `CC377A9928C7BB18` — confirmed importable via `gpg --list-secret-keys --keyid-format LONG` on this machine) is read from the same properties file automatically; you do not pass it separately.\n5. Transfer to Central Portal:\n   ```bash\n   curl --fail --request POST -H \"Authorization: Bearer $CENTRAL_BEARER\" \\\n     'https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/io.github.sanchitmonga22?publishing_type=automatic'\n   ```\n   Then find the resulting `portal_deployment_id` — expect **exactly one** fresh repository; if the count isn't 1, stop and investigate rather than guessing which one is yours (see the doc for the exact `jq` filter).\n6. Poll `https://central.sonatype.com/api/v1/publisher/status?id=<id>` (POST, same bearer) until `deploymentState` is `PUBLISHED`. Automatic deployments normally go `PENDING -> VALIDATING -> PUBLISHING -> PUBLISHED` within a few minutes. **If it lands on `VALIDATED` instead of proceeding, that means Sonatype is waiting for a manual click in the Central Portal UI** — stop and tell the human; do not loop forever assuming it resolves itself. On `FAILED`, print `.errors` and stop.\n7. Verify propagation (allow 10-30 minutes for CDN propagation even after `PUBLISHED`):\n   ```bash\n   for a in runanywhere-sdk runanywhere-llamacpp runanywhere-onnx; do\n     echo \"$a: $(curl -s -o /dev/null -w '%{http_code}' \"https://repo1.maven.org/maven2/io/github/sanchitmonga22/$a/$VERSION/$a-$VERSION.pom\")\"\n   done\n   ```\n   Expect `200` for all three.\n\n## 3. pub.dev (Flutter)\n\n**Verify the current package set fresh — do not trust a hardcoded count.** As of this writing the real, non-QHexRT Flutter package set is **4**, not 3: `runanywhere`, `runanywhere_llamacpp`, `runanywhere_onnx`, and `runanywhere_mlx` (mlx was added after some earlier documentation/memory of \"3 packages\" was written — that assumption is stale; `runanywhere_mlx` is a real, already-published-on-pub.dev package, not new/WIP). Enumerate it yourself instead of trusting this list verbatim next time:\n```bash\nfind bindings/flutter/packages -maxdepth 1 -type d -not -iname '*qhexrt*' -not -path '*/packages'\n```\n`runanywhere_qhexrt` exists in that directory too — never publish it (same exclusion policy as Maven).\n\nStage the released iOS xcframeworks + Android `.so` into each package with the Flutter equivalent of `package-sdk.sh` (melos-based monorepo, confirmed at `bindings/flutter/scripts/package-sdk.sh`):\n\n```bash\nbash bindings/flutter/scripts/package-sdk.sh --mode local --natives-from <dir-with-xcframeworks-and-android-.so>\n```\n\nThis script already runs `flutter pub publish --dry-run` internally per package as part of its own validation — but run it again yourself, per package, from each package directory, and **read the output for real, not just the exit code**:\n\n```bash\ncd bindings/flutter/packages/runanywhere && flutter pub publish --dry-run\ncd ../runanywhere_llamacpp && flutter pub publish --dry-run\ncd ../runanywhere_onnx && flutter pub publish --dry-run\ncd ../runanywhere_mlx && flutter pub publish --dry-run\n```\n\nExpect zero warnings except a benign version-gap hint. Only then run the real publish, same order (core first, since the others depend on it):\n\n```bash\ncd bindings/flutter/packages/runanywhere && flutter pub publish\ncd ../runanywhere_llamacpp && flutter pub publish\ncd ../runanywhere_onnx && flutter pub publish\ncd ../runanywhere_mlx && flutter pub publish\n```\n\n**Credential note:** pub.dev OAuth is normally cached at `~/Library/Application Support/dart/pub-credentials.json` on macOS (confirmed present on this machine). If it's missing, expired, or `flutter pub publish` prompts for an interactive browser login, **STOP and tell the human** — you cannot complete a fresh Google OAuth flow non-interactively, and forcing/guessing here is not appropriate.\n\nVerify afterward against the live API (not a cached page):\n```bash\nfor p in runanywhere runanywhere_llamacpp runanywhere_onnx runanywhere_mlx; do\n  echo \"$p: $(curl -s \"https://pub.dev/api/packages/$p\" | python3 -c \"import json,sys;print(json.load(sys.stdin)['latest']['version'])\")\"\ndone\n```\n\n## 4. Swift SPM distribution repo (`RunanywhereAI/runanywhere-swift`)\n\nA separate, **generated-only** repo mirroring `bindings/swift` (Package.swift + Sources/ + LICENSE + README) so SwiftPM consumers using `from: \"<version>\"` don't clone the ~340MB monorepo.\n\n**GOTCHA:** this machine may have local directories literally named `runanywhere-swift` that are just checkouts of the **monorepo's** remote, not the distribution repo. Verify before trusting any existing local directory by name:\n```bash\ngit -C <candidate-dir> remote get-url origin\n```\nMust show `RunanywhereAI/runanywhere-swift`, not `runanywhere-sdks`. Always clone fresh instead of guessing:\n\n```bash\ngit clone https://github.com/RunanywhereAI/runanywhere-swift.git /tmp/ra-swift\n\nbindings/swift/scripts/sync-dist-repo.sh \\\n  --zips <exact-verified-Apple-archive-dir-from-the-release> --tag /tmp/ra-swift\n\nRUNANYWHERE_SWIFT_DIST_REPO=/tmp/ra-swift \\\n  bash scripts/validation/gates/check_swift_dist_repo_sync.sh\n\ngit -C /tmp/ra-swift push origin main --follow-tags\n```\n\nThe `--zips` dir must contain the verified (sha256-checked) release archives — `RACommons-ios-v$VERSION.zip`, `RABackend{LLAMACPP,MLX,NeuRT,ONNX,Sherpa}-ios-v$VERSION.zip`, `RunAnywhereMLX{Metal,Resources,Runtime}-ios-v$VERSION.zip` — downloaded and checked exactly per the general verification discipline, not re-used from an earlier, untrusted local copy.\n\n**The tag on `runanywhere-swift` is bare semver, no `v` prefix** (SwiftPM's `from:` needs that) — do not tag it `v$VERSION` by mistake; `sync-dist-repo.sh --tag` handles this correctly, but double check the pushed tag with `git -C /tmp/ra-swift tag --points-at HEAD` before walking away.\n\nThis is enforced, not just documented: once `v$VERSION` is tagged on `runanywhere-sdks`, `check_swift_dist_repo_sync.sh` fails every future PR on the monorepo until `runanywhere-swift` carries the matching bare-semver tag — so don't let this lag behind the other registries.\n\n**GATE GAP — `check_swift_dist_repo_sync.sh` does NOT catch dependency drift, only checksum/version-string drift.** `runanywhere-swift`'s `Package.swift` is manually maintained end to end — `sync-dist-repo.sh` only ever touches `sdkVersion` and the binaryTarget checksums, never the `dependencies:` block (mlx-swift, mlx-swift-lm, mlx-audio-swift, swift-crypto, etc.). If root `runanywhere-sdks/Package.swift` bumps one of those (e.g. a new fork tag), a human has to remember to mirror it into the dist repo by hand, and nothing fails CI if they forget. This exact drift shipped once (2026-08-17): the dist repo pointed `mlx-swift-lm` at unforked `ml-explore/mlx-swift-lm` instead of the `RunanywhereAI` fork, silently breaking every iOS/macOS MLX consumer of that tag until caught by an actual build failure ~40 minutes later. Before trusting a freshly-cut dist-repo tag: diff every `.package(url:` line between root `Package.swift` and the dist repo's `Package.swift` (`grep -n '\\.package(url:' Package.swift` on both sides), and actually build a real product that exercises the dependency you're worried about (e.g. `cd /tmp/ra-swift && swift build --product RunAnywhereMLX`) — don't rely on the sync gate alone. If you catch drift on a tag that's only minutes old with no evidence of external consumption, fixing it in place and force-moving the tag (`git tag -f $VERSION && git push origin $VERSION --force`) is the correct call, not a shortcut — a known-broken immutable tag helps no one.\n\n**If you force-move a tag, SwiftPM/Xcode consumers cache the OLD commit in several places — clearing just `Package.resolved` is not enough.** Verifying your own fix (or having a downstream consumer verify it) can silently keep failing even after the fix is correctly pushed, because: (1) any consumer repo's own tracked `*.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved` pins the exact old resolution and `xcodebuild -resolvePackageDependencies` respects it rather than re-resolving; (2) `~/Library/Caches/org.swift.swiftpm/repositories/<pkg>-<hash>/` is a global shared clone cache keyed by repo, oblivious to a moved tag; (3) `~/Library/org.swift.swiftpm/security/fingerprints/<pkg>-<hash>.json` records the fingerprint SwiftPM saw for that tag and can silently keep using it after a mismatch instead of failing loud. To actually re-resolve after moving a tag: delete the consumer's tracked `Package.resolved`, `rm -rf .build .swiftpm` and any `~/Library/Developer/Xcode/DerivedData/<Scheme>-*`, AND clear the matching entries under both global `org.swift.swiftpm` cache directories for every affected package identity — then resolve fresh.\n\n## 5. macOS rcli notarization\n\nCI (`native_rcli_macos` in `release.yml`) usually already produces a notarized, stapled artifact when Developer ID + notary secrets are configured in repo secrets. **Check the release assets first** before assuming you need to notarize anything yourself locally:\n\n```bash\ngh release view v$VERSION --json assets --jq '.assets[].name' | grep -iE 'rcli-macos|\\.dmg'\n```\nBaseline expectation is `rcli-macos-arm64-v$VERSION.tar.gz` (+ `.sha256`); a `.dmg` only appears when `RCLI_MACOS_FULL_RELEASE=1` was set for that build (see below). If a plain tarball is already present and its contents pass the general verification discipline, there is usually nothing further to do here.\n\n**If you do need to notarize locally**, everything lives in `rcli/scripts/package-rcli.sh` (read its header comment — it documents its own contract in detail):\n\n- One-time setup, check before redoing: `xcrun notarytool history --keychain-profile runanywhere-notary` (confirmed configured on this machine — a fresh submission history returns without error). If missing: `xcrun notarytool store-credentials \"runanywhere-notary\" --apple-id <email> --team-id <team> --password <app-specific-password>`.\n- Codesign identity: `security find-identity -v -p codesigning | grep \"Developer ID Application\"` — confirmed present on this machine as `\"Developer ID Application: RunAnywhere, Inc (<TEAM_ID>)\"`. Grep for it fresh rather than hardcoding the team-id suffix, it can rotate.\n- Invocation contract (env vars, from the script header): `RCLI_CODESIGN_IDENTITY`, `RCLI_MACOS_NOTARIZE=1`, `RCLI_MACOS_FULL_RELEASE=1`, `RCLI_MACOS_SWIFT_BIN_DIR=<output of build-mlx-cli.sh>`. `RCLI_MACOS_FULL_RELEASE=1` is what additionally stages `mlx.metallib` and SwiftPM resource bundles and is what causes a notarized, stapled `.dmg` to be emitted alongside the Homebrew-style tarball. `RCLI_CODESIGN_KEYCHAIN` / `RCLI_NOTARYTOOL_KEYCHAIN` matter only if the identity/profile live in a non-default keychain.\n- Call shape: `rcli/scripts/package-rcli.sh <build-dir> macos-arm64` with the env vars above exported.\n\n## 6. Electron — build AND packaging are now in `release.yml`; publish is still manual\n\n**Reversing the earlier \"deliberately separate\" note**: as of `fix/electron-cicd-staging-url-npu-ane` (2026-08-21), the repo owner asked for Electron to produce ready-to-publish artifacts through the real release train like every other SDK, so `release.yml` now has `native_electron` (invokes `.github/workflows/electron-native-package.yml` as a reusable `workflow_call`) and `sdk_electron` (downloads its `electron-packaged-tarballs` artifact, runs `scripts/release/prepublish_check.py` on every tarball, checksums them, and splits public vs. QHexRT-private). `publish` hard-gates on `sdk_electron` succeeding. `@runanywhere/electron`, `-llamacpp`, `-onnx`, `-sherpa`, `-neurt` land as real, `.sha256`-sidecarred GitHub Release assets, same as the Web/RN npm tarballs. `@runanywhere/electron-qhexrt` still never becomes a public Release asset (policy below) — it's produced and packaged every release, but only reachable as the `sdk-electron-qhexrt-private` workflow artifact on that run, for a human to `npm publish` by hand. Electron is **not** WIP on a branch anymore: the old root `AGENTS.md` \"Active issues\" section describing `smonga/electron_upgrade` as in-progress is gone entirely as of this checkout — `bindings/electron/` is a fully merged, real SDK on `main`. Re-check that section is still absent before trusting this note.\n\n`.github/workflows/electron-native-package.yml` itself is unchanged in what it builds: every backend for real (llamacpp/onnx/sherpa/neurt on their real platforms, qhexrt on a self-hosted Windows ARM64 runner) with the real `STAGING_BASE_URL` baked in, gated by `check_plugin_natives.py` (which fails closed on the staging-URL placeholder — the exact bug that shipped through `0.20.24`). What's new is that it's also invocable via `workflow_call`, and that `native_win_x64_and_package` now tolerates a failed/skipped QHexRT lane (self-hosted, single machine) rather than losing the other five real packages to one runner's outage. **`release.yml` itself still does not `npm publish` anything** — it only produces artifacts (Release assets for the public five, a workflow artifact for QHexRT) — same \"build automated, publish manual\" shape as every other platform. See `thoughts/shared/plans/electron_cicd_staging_url_and_npu_ane_pipeline.md` for the full history.\n\n**PROVEN END TO END on v0.20.25 (2026-08-22)** — the first release where this actually ran. Final state: 65 release assets, all 5 public Electron tarballs present with `.sha256` sidecars, zero `qhexrt`-named assets. Verified by downloading the *published* tarballs and running the repo's own gate against them:\n\n```bash\ngh release download v<version> --repo RunanywhereAI/runanywhere-sdks -p 'runanywhere-electron*.tgz' -D /tmp/verify\npython3 scripts/validation/gates/check_plugin_natives.py --expect-version <version> \\\n    $(for t in /tmp/verify/*.tgz; do printf -- '--tarball %s ' \"$t\"; done)\n# => All 20 check(s) passed: plugin natives are routable and no staging-URL placeholder was found.\n```\n\nThat single command asserts all three things you actually care about, per binary: the real `STAGING_BASE_URL` is baked in (no `YOUR_STAGING_BASE_URL`), `rac_commons` embeds the right version, and every backend is genuinely **routable** (`g_llamacpp_ops`, `g_sherpa_stt_ops`, `g_neurt_llm_ops`, `g_onnx_embeddings_ops`, …) rather than a same-sized shell. **Run it before every Electron `npm publish`** — it is the cheapest defense against exactly the class of defect that shipped `electron-sherpa` 0.20.17's NULL-vtable carrier. Expected shipped layout: entry package = `darwin-arm64` + `win32-x64` + `win32-arm64`; llamacpp/onnx/sherpa = `darwin-arm64` + `win32-x64`; neurt = `darwin-arm64` only; qhexrt = `win32-arm64` only.\n\nWhere to get the QHexRT tarball for its manual publish (it is deliberately NOT a release asset):\n\n```bash\ngh run download <release-run-id> --repo RunanywhereAI/runanywhere-sdks \\\n    --name sdk-electron-qhexrt-private --dir /tmp/qhexrt\n```\n\nIf asked to actually publish an Electron package (still a real, separate, manual step — this skill's general discipline in §0/§General verification still applies in full):\n- Involve the Windows box for real Windows-native verification before publishing anything Windows-targeted. The team keeps a **Windows ARM64 machine reachable over SSH**; its hostname, user, key, and mesh-VPN node name are operator-specific and deliberately not recorded in this repo — read the Windows/ARM64 host entry out of the operator's `~/.ssh/config` and use that alias. If there is no such entry, the box is not configured for you: say so and ask, rather than guessing at a hostname. Check reachability **first** by asking the mesh-VPN client whether the node is up (e.g. `tailscale status`, matching on the host entry's `HostName`) — if it reports `offline, last seen ...`, the box is asleep, which is not a configuration problem: ask the human to wake it, do not debug it as broken. Only then `ssh -o ConnectTimeout=8 \"$WIN_BOX\" \"whoami\"`. That same machine is **also** registered as a GitHub Actions self-hosted runner (labels `[self-hosted, Windows, ARM64, qhexrt]`) running as a Windows service — a queued `native_win_arm64_qhexrt` job on it means the machine is busy; check `gh api repos/RunanywhereAI/runanywhere-sdks/actions/runners` for the matching runner's `busy`/`status` before assuming it's free for manual SSH work, and vice versa.\n- **`@runanywhere/electron-qhexrt` being public on npm is a CONFIRMED, settled policy decision** (repo owner, 2026-08-20) — it contains real Hexagon NPU binaries (`libQnnHtpV81Skel.so`, `QnnHtp*.dll`, `rac_commons.dll`, `runanywhere_qhexrt.dll`) on purpose. The QHexRT-stays-private policy enforced everywhere else in this skill is specifically about the **GitHub-Release-side** asset set (Android etc.), a different channel — not a contradiction. Stop treating this as an unresolved item to flag; only re-raise it if you find evidence the policy changed.\n\n## 7. Final sanity pass\n\nAfter every registry is done, re-view each package's public listing/API and compare against the previous version — a big unexplained drop in file count or package size is a red flag to investigate before calling the release fully published:\n\n```bash\n# npm\nfor p in proto-ts web web-llamacpp web-onnx core llamacpp mlx onnx; do\n  npm pack @runanywhere/$p@$VERSION --dry-run 2>&1 | tail -5\ndone\n\n# Maven — check module metadata / POM sizes are non-trivial, not near-empty\nfor a in runanywhere-sdk runanywhere-llamacpp runanywhere-onnx; do\n  curl -s -o /dev/null -w \"%{size_download} bytes  $a\\n\" \\\n    \"https://repo1.maven.org/maven2/io/github/sanchitmonga22/$a/$VERSION/$a-$VERSION.aar\"\ndone\n\n# pub.dev\nfor p in runanywhere runanywhere_llamacpp runanywhere_onnx runanywhere_mlx; do\n  curl -s \"https://pub.dev/api/packages/$p\" | python3 -c \"import json,sys;d=json.load(sys.stdin);print('$p', d['latest']['version'])\"\ndone\n```\n\nOnly after this pass — every registry shows `$VERSION`, every artifact's contents check out, and nothing regressed in size/file-count versus the previous release — is the release fully published. Do not skip this step just because each individual publish call reported success; a successful upload is not the same claim as \"the right bytes are now live.\"\n\n<!-- File: .claude/skills/sdk-release/SKILL.md (Tokens: ~13119 | Category: anthropic-skill) -->\n---\nname: sdk-release\ndescription: Cut a new RunAnywhere SDK release end-to-end in RunanywhereAI/runanywhere-sdks — version bump through a fully green, tagged, drafted-then-published GitHub Release. Stops BEFORE publishing to package registries (npm/Maven/pub.dev/SwiftPM dist) — that is the separate \"sdk-publish\" skill, invoked as the last step here.\n---\n\n# SDK Release (runanywhere-sdks)\n\nRepo: `~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks` (verify with `pwd`/`ls` —\nthis is a workspace subfolder, not the repo root of the whole Qualcomm/ tree, and paths\ndrift; re-check script paths below still exist before trusting them blindly).\n\nThis is a **runbook**, not a description. Follow the steps in order. Every `gh`/`git`\ncommand below is meant to be run close to verbatim — adjust only the version/PR/run-id\nvalues for the release you're actually cutting.\n\n**THIS SKILL HAS BEEN SKIPPED THREE TIMES IN A ROW (v0.20.29, v0.20.30, v0.20.31),\nproducing the IDENTICAL failure each time, DESPITE step 2/3 below documenting the exact\nfix since the v0.20.25/v0.20.29 incidents.** All three times, an agent ran\n`sync-versions.sh`, opened the PR, merged it, and pushed the tag directly — skipping the\n\"dispatch a release candidate BEFORE merging, sync checksums from it, THEN merge\" sequence\nin steps 2-3 — usually because the version bump was one step inside a longer autonomous\nchain (a downstream repo re-pin, a multi-repo release cascade) and felt like \"just a\nversion bump,\" not a release. **A version bump to `core/VERSION` IS a release. If you are\nabout to run `scripts/release/sync-versions.sh` for ANY reason — including re-pinning this\nrepo as a step in some OTHER repo's release — stop and follow this skill from step 1,\nincluding the candidate dispatch. Do not improvise the sequence from memory even if you\ndid it correctly last time; do not skip it because \"this bump is small\" or \"we're in a\nhurry.\"** v0.20.31 additionally caught a second, previously-undetected instance of the\nsame class of bug in the SAME commit: `RunAnywhereMLXRuntime` in the Flutter podspec (which\n`RAC_CHECKSUM_SKIP` exempts from CI verification, so it fails silently for a consumer\ninstead of failing `publish`) was ALSO stale, and had to be independently re-verified\nagainst the same candidate build — see step 3's checksum-location table; check every row\nin it, not just the one CI complained about.\n\nCompanion skill: **sdk-publish** — invoke it at the very end (step 10) for the actual\nnpm / Maven / pub.dev / SwiftPM-distribution publishing. This skill's job stops at a\npublished (non-draft) GitHub Release with correct assets.\n\n---\n\n## 0. Orientation facts (confirm these still hold before relying on them)\n\n- Canonical version file: `core/VERSION` (single line, e.g. `0.20.24`). `core/VERSIONS`\n  also carries a `PROJECT_VERSION=` line that must agree — `sync-versions.sh` updates\n  both, `auto-tag.yml` reads `core/VERSIONS`, `check_release_version_coherence.sh` reads\n  `core/VERSION`. Don't hand-edit either; always go through the script.\n- `scripts/release/sync-versions.sh <version>` — bumps ~20 files (Package.swift,\n  gradle.properties, all package.json/pubspec.yaml, Kotlin/Swift/TS version constants,\n  AGENTS.md). Accepts a leading `v` and strips it.\n- `scripts/validation/gates/check_release_version_coherence.sh` — the gate that enforces\n  \"PR changing VERSION must carry exactly one `release:patch|minor|major` label, and the\n  new version must be exactly base+1 for that bump type.\" Reads `PR_BASE_SHA` and\n  `PR_RELEASE_LABELS_JSON` env vars when run under CI; locally it only checks the\n  canonical-version-format part.\n- `.github/workflows/pr-build.yml` job **`centralization`** is what actually runs the\n  coherence gate in CI (`Release-train version coherence` step), alongside\n  `check_typescript_centralization.sh`, `check_flutter_centralization.sh`,\n  `check_gradle_centralization.sh`, `check_wasm_provenance_contract.sh`,\n  `check_swift_dist_repo_sync.sh`, `check_agents_claude_sync.sh`,\n  `check_no_hardcoded_defaults.sh`. This job's trigger types are\n  `[opened, synchronize, reopened, labeled, unlabeled]` — **adding the release label\n  after opening the PR fires a fresh run**, which is how the gate clears without a new\n  commit (see step 1).\n- `.github/workflows/release.yml` — the build/package/draft-release workflow. Triggers:\n  tag push `v*.*.*` OR `workflow_dispatch` (works on **any branch**, not just tags) with\n  inputs `version` (required), `validate_external_starters` (bool, default false),\n  `publish_from_run_id` (optional), `reuse_native_web_run_id` (optional).\n- `.github/workflows/auto-tag.yml` — fires on `pull_request: closed` to `main` when the\n  merged PR carries a `release:*` label. Verifies the version bump matches the label,\n  pushes the `v<version>` tag, and **explicitly dispatches a fresh `release.yml` run at\n  that tag** (plain `git push` of a tag with `GITHUB_TOKEN` does not auto-trigger\n  workflows, so it calls `gh workflow run release.yml --ref v<version> -f version=<version>`\n  itself). This is the redundant rebuild you must cancel — see step 7.\n- Branch protection on `main`: check\n  `gh api repos/RunanywhereAI/runanywhere-sdks/branches/main/protection` — 2 approvals +\n  code owner review required, but look for a `bypass_pull_request_allowances` (users/teams/apps)\n  list. If your `gh api user --jq .login` is on that list, `gh pr merge --squash --admin`\n  is a legitimate, pre-configured bypass — not a hack. If not on the list, stop and ask a\n  human to approve/merge.\n- Never let any `qhexrt`-named asset reach the public asset set. `release.yml`'s own\n  manifest-assertion step (`find release-flat -maxdepth 1 -type f -iname '*qhexrt*'`)\n  hard-fails the run if one is found — this is enforced, not just policy.\n- **Electron IS part of `release.yml` now** (as of `fix/electron-cicd-staging-url-npu-ane`,\n  2026-08-21) — this reverses the prior \"deliberately separate\" policy noted below by an\n  earlier pass; the repo owner explicitly asked for Electron to produce ready-to-publish\n  artifacts through the real release train like every other SDK. `native_electron`\n  invokes `.github/workflows/electron-native-package.yml` as a **reusable workflow**\n  (`workflow_call`) rather than duplicating its ~400 lines of hard-won native-build logic\n  inline; `sdk_electron` downloads the resulting `electron-packaged-tarballs` artifact,\n  runs `scripts/release/prepublish_check.py` on every tarball, computes checksums, and\n  splits public vs. QHexRT-private before anything reaches `release-flat`. `publish`\n  hard-gates on `sdk_electron` succeeding (same tier as `sdk_kotlin`/`sdk_web`/\n  `sdk_react_native`) but treats `native_electron`'s own result as advisory, because that\n  job's result folds in `native_win_arm64_qhexrt` — a single, personally-owned\n  self-hosted NPU runner whose outage should cost only `@runanywhere/electron-qhexrt`\n  for that release, not the whole SDK train (`native_win_x64_and_package`, the job that\n  actually produces the tarballs, tolerates a failed/skipped QHexRT lane and still packages\n  the other five real artifacts). Like every other SDK here, this still does **not**\n  `npm publish` anything — it only produces the tarballs as GitHub Release assets\n  (public ones) / a workflow artifact (the private QHexRT one) for a human to publish by\n  hand, matching the \"build automated, publish manual\" shape everywhere else.\n  **Electron is no longer WIP on a branch** — the old root `AGENTS.md`/`CLAUDE.md`\n  \"Active issues\" section describing `smonga/electron_upgrade` as in-progress is gone\n  entirely as of this checkout; `bindings/electron/` is a fully merged, real SDK on\n  `main` with its own `AGENTS.md`. Re-check that section is still absent before trusting\n  this note — if it has reappeared, something regressed.\n  See `thoughts/shared/plans/electron_cicd_staging_url_and_npu_ane_pipeline.md` for the\n  full history (this pipeline shipped the placeholder-URL bug three times before\n  `electron-native-package.yml` existed).\n  **QHexRT public/private boundary — CONFIRMED POLICY, not an open flag**: the repo\n  owner explicitly confirmed (2026-08-20) that `@runanywhere/electron-qhexrt` stays\n  public on npm intentionally (it already is, and is a real, working precedent worth\n  studying before touching QHexRT packaging elsewhere) — the QHexRT-stays-private\n  policy enforced everywhere else in this pipeline is specifically about the\n  **GitHub-Release-side** asset set (e.g. Android), a different distribution channel.\n  This is settled; stop re-flagging it as an unresolved contradiction. Only re-raise it\n  if you find evidence the policy has changed.\n\n---\n\n## 0b. NeuRT is a PINNED PREBUILT now — check this before anything else\n\n`native_ios` no longer compiles the private `neurun` repo. It downloads archives that\nrepo publishes, pinned by tag + SHA-256 in `core/VERSIONS`, the same shape as sherpa-onnx:\n\n```\nNEURUN_REPO=RunanywhereAI/neurun      # shared: one repo publishes BOTH engines\nNEURT_RELEASE_TAG=v<version>          # per-engine: a release may carry one lane only\nNEURT_RAC_ABI_VERSION=9\nNEURT_MACOS_ARM64_SHA256=…\nNEURT_IOS_ARM64_SHA256=…\nNEURT_IOS_ARM64_SIMULATOR_SHA256=…\n```\n\n`NEURUN_REPO` is shared because one neurun release carries both engines. The **tags stay\nper-engine** on purpose: that repo can cut a release for one lane only, so a given tag may\ncarry just one engine's assets.\n\n**Run the gate after re-pinning** — it verifies the pins against the release itself, so a\nmistyped or locally-computed value fails there instead of mid-build:\n\n```bash\nNEURUN_TOKEN=\"$(gh auth token)\" bash scripts/validation/gates/check_engine_prebuilt_pins.sh\n```\n\n### Local engine development — there is no source mode\n\nThe engine has no `NEURT_ROOT` path any more; nothing here compiles engine source. To iterate\non NeuRT, build a slice in the neurun checkout and point the engine at it — no release, no\ntag, no pin edit:\n\n```bash\n# in neurun\nNeuRT/tools/scripts/package-rac-dist.sh --sdk-root <this repo> --version 0.0.0-dev \\\n    --slice macos-arm64 --out /tmp/devdist\nmkdir -p /tmp/devslice && tar -xzf /tmp/devdist/neurt-macos-arm64-v0.0.0-dev.tar.gz \\\n    -C /tmp/devslice --strip-components=1\n\n# here\nNEURT_PREBUILT_ROOT=/tmp/devslice cmake -S . -B build/dev -G Ninja \\\n    -DRAC_BUILD_BACKENDS=ON -DRAC_BACKEND_NEURT=ON -DRAC_RUNTIME_COREML=ON\n# expect: Mode: PREBUILT (macos-arm64, ABI 9)\n```\n\n`NEURT_PREBUILT_ROOT` is a **single-slice** root, which is why\n`build-core-xcframework.sh` deliberately ignores it: packaging needs all three slices, so\nhonouring a single-slice override there would let one staged slice satisfy the guard and\nsilently package two non-routable stubs. Use an obviously-not-a-release version like\n`0.0.0-dev` so a dev slice can never be mistaken for a published one.\n\n**Why this changed, and why it matters to you specifically.** `engines/neurt/CMakeLists.txt`\nused to resolve a `NEURT_ROOT` checkout of that private repo and **regex-parse its root\n`CMakeLists.txt`** for a source list. Under that arrangement, commons widened\n`rac_llm_stream_callback_fn` with `tokens_in_delta`, the pinned neurun commit still passed\n4 arguments, and `native_ios` died with *\"too few arguments to function call, expected 5,\nhave 4\"* on **v0.20.15, v0.20.16 and v0.20.17**. Every packaging job needs `native_ios`,\nso all three shipped **iOS-only — 18 assets where a healthy release has 55+**. That is the\nsingle most expensive failure this runbook exists to prevent, and it came from this seam.\n\n**Before opening the release PR**, confirm the pin is current:\n\n```bash\ngrep -A5 '^NEURT_REPO' core/VERSIONS\ngh release view \"$(grep '^NEURT_RELEASE_TAG=' core/VERSIONS | cut -d= -f2)\" \\\n    --repo RunanywhereAI/neurun --json assets --jq '.assets[].name'\n```\n\nExpect six assets — three `neurt-<slice>-v<version>.tar.gz` plus their `.sha256` sidecars.\n\n**If the plugin ABI changed this cycle** (`RAC_PLUGIN_API_VERSION` in\n`core/include/rac/plugin/rac_plugin_entry.h`), the pinned archives are invalid and\n`native_ios` will fail at the download step. Cut a neurun release first — that repo's\n`release_sdk_archives` skill is the runbook — then re-pin both `NEURT_RELEASE_TAG` and\n`NEURT_RAC_ABI_VERSION` here. The downloader refuses to run if `NEURT_RAC_ABI_VERSION`\ndisagrees with the header, so this cannot be half-done.\n\nProve it locally before trusting a tag build:\n\n```bash\nNEURUN_TOKEN=\"$(gh auth token)\" bash scripts/build/download-neurt.sh --all\n```\n\nThat verifies each tarball's SHA-256 **and** that its `RECEIPT.json` names the same ABI\nversion this repo defines. Then check configure prints `Mode: PREBUILT (<slice>, ABI N)`.\n`SOURCE` means a local neurun checkout was picked up (prebuilt is meant to win, so the\ndownload did not land); `SHELL` means the engine is **not routable** and a release built\nnow would ship an engine that registers, claims Apple availability and serves nothing.\n\n`release.yml` treats the downloader's exit **3** (\"no token — degrade to the shell\") as\n**fatal**. That degrade is right for a fork PR and catastrophic for a release.\n\n### The complete engine matrix — who consumes what, and how\n\nThere are only **three fetch points in the entire repo**. Everything else consumes an\nsdks artifact, which is why a single fetch fix can repair five SDKs at once — and why a\nsingle missing fetch can silently break several.\n\n```\nneurun release (private)\n  ├─ native_ios         ──NeuRT x3 slices──►  RABackendNeuRT.xcframework\n  │                                              ├─ Swift  (Package.swift binary target)\n  │                                              ├─ Flutter / React Native (co-vendored\n  │                                              │   in the onnx package's podspec)\n  │                                              └─ rcli-macos (stages it, then GREPS\n  │                                                  `backends --json` for neurt)\n  ├─ native_android     ──QHexRT arm64-v8a──►  one build stages jniLibs into\n  │                                              Kotlin + React Native + Flutter\n  └─ electron           ──NeuRT macos-arm64 AND QHexRT win-arm64──►  its own natives\n```\n\n| target | NeuRT | QHexRT | note |\n|---|---|---|---|\n| Swift (iOS/macOS) | ✅ xcframework | — | NeuRT is Apple-only |\n| Flutter / React Native (Apple) | ✅ xcframework | — | co-vendored in the onnx package |\n| Flutter / React Native (Android) | — | ✅ jniLibs | from the same Android build as Kotlin |\n| Kotlin (Android) | — | ✅ jniLibs | arm64-v8a only; the only ABI with a Hexagon NPU |\n| Electron macOS | ✅ fetches | — | builds its own natives |\n| Electron win-arm64 | — | ✅ fetches | |\n| rcli macOS | ✅ xcframework | — | **best check in the tree** (below) |\n| rcli linux / windows-x64 | — | — | correct: NeuRT is Apple-only, QHexRT is win-arm64 |\n\n**`rcli-macos` is the strongest verification that exists here.** It greps\n`backends --json` for `\"name\":\"neurt\"` — and a non-routable shell *refuses\nregistration*, so that grep proves the engine is **routable**, not merely present. Copy\nthis pattern when adding an engine rather than checking for a file on disk.\n\n**Two silent-failure traps this matrix exists to prevent:**\n\n- **`RAC_BACKEND_<X>=ON` does not mean the engine works.** Without a payload the engine\n  builds as a non-routable shell and registration is refused — no build error. Android\n  shipped that way for months, and `rcli-macos-release` still sets\n  `RAC_BACKEND_NEURT: ON` regardless of whether a payload was fetched.\n- **`copy_if_exists` in `build-core-android.sh` silently no-ops** when a lib was never\n  built. That is the same guarded-copy pattern that shipped Windows with zero engines for\n  several releases. The release now asserts `librunanywhere_qhexrt.so` is really inside\n  the arm64-v8a archive.\n\n`scripts/validation/gates/check_engine_prebuilt_pins.sh` asserts all of the above plus\nthe pins, the ABI, header drift against the receipt, and (with a token) the release\ncontents. Run it before cutting a release; it is also wired into `pr-build` offline.\n\n### QHexRT is pinned the same way — check it too\n\nThe Hexagon-NPU engine crosses the identical boundary, published by the same neurun release:\n\n```\nQHEXRT_RELEASE_TAG=v<version>\nQHEXRT_ARM64_V8A_SHA256=…      QHEXRT_ARM64_V8A_RECEIPT=…\nQHEXRT_WIN_ARM64_SHA256=…      QHEXRT_WIN_ARM64_RECEIPT=…\n```\n\nTwo pins per ABI, not one: the tarball checksum **and** the payload's own build-receipt\nhash. The receipt is the directory name under `engines/qhexrt/prebuilt/versions/`, so\npinning it is what makes the pin describe the bytes rather than trusting the archive's\nown claim about itself.\n\n```bash\nNEURUN_TOKEN=\"$(gh auth token)\" bash scripts/build/download-qhexrt.sh --abi arm64-v8a\nNEURUN_TOKEN=\"$(gh auth token)\" bash scripts/build/download-qhexrt.sh --abi win-arm64\n```\n\n**`current` selects ONE ABI at a time.** Both payloads can sit in `versions/` together, but\na build targets one platform — so re-run the downloader when switching between an Android\nand a Windows build. The downloader ends by running `validate-qhexrt-prebuilt.py` itself,\nso a bad payload fails at the pin rather than mid-build.\n\n**The QHexRT lane in neurun is deliberately non-blocking.** It runs on a single\nself-hosted Snapdragon box (QAIRT is licence-gated and exists on no hosted runner). If\nthat machine is offline, neurun's release publishes the Apple artifacts and **warns** that\nno QHexRT payload was produced — and the SDK simply keeps its previously pinned prebuilt.\nThat is correct behaviour, not a failure: do not block an SDK release on it. Only treat it\nas blocking if this release is specifically meant to ship new NPU kernels.\n\n**Do not hand-stage `engines/qhexrt/prebuilt/`.** That was the old flow, and it is how the\nElectron win-arm64 lane once pinned a receipt staged months earlier and silently shipped\nNPU kernels from an old commit. If the tree looks stale, re-pin and re-download; never\ncopy a payload in by hand.\n\n---\n\n## 1. Decide the version and open the release PR\n\nCheck for an already-open release PR first — don't duplicate work:\n\n```bash\ngh pr list --repo RunanywhereAI/runanywhere-sdks --label release:patch --label release:minor\n```\n\n(gh ORs multiple `--label` flags into a single AND-filtered query per invocation is not\nguaranteed across all gh versions — if unsure, run the two label queries separately:\n`--label release:patch` then `--label release:minor`, plus `--label release:major`.)\n\nPick a patch/minor/major bump based on the changes since the last tag. Then:\n\n```bash\ngit checkout -b release/v<version>\n./scripts/release/sync-versions.sh <version>      # e.g. 0.20.25 (no 'v' needed, either works)\nbash scripts/validation/gates/check_release_version_coherence.sh\n```\n\nThe coherence script may point out generated locks/changelogs that need regenerating\n(e.g. package-lock.json version bumps it expects but didn't find, or a missing CHANGELOG\nheading for the new version). Fix everything it flags, and add **explicit, human-written\nchangelog entries** — the gate requires a heading for the new version to exist; it does\nnot write prose for you.\n\nOpen the PR and **apply exactly one `release:patch` or `release:minor` (or `release:major`)\nlabel** — this is required, not optional. Two failure modes to know:\n\n- If you forget the label and push a commit later, the `centralization` job's\n  `Release-train version coherence` step fails.\n- **Adding the label after the PR is already open does NOT get picked up by the run that\n  already executed.** Because `pr-build.yml`'s `centralization` job triggers on\n  `labeled`/`unlabeled` PR events, applying the label fires a **new** run of the whole\n  job — that new run is what goes green. Don't push an empty commit to \"retrigger\" it;\n  just add/re-add the label (or use `gh pr edit <pr> --add-label release:patch`, and if it\n  was already present, remove + re-add to force the event).\n\n---\n\n## 2. Get REAL native-artifact checksums BEFORE merging — dispatch a release candidate\n\nDo not merge on faith that native builds will succeed. `release.yml`'s\n`workflow_dispatch` trigger works on **any branch**, including your unmerged release\nbranch — use it to build a full release candidate first:\n\n```bash\ngh workflow run release.yml --ref release/v<version> -f version=<version>\n```\n\nGet the run id and watch it:\n\n```bash\ngh run list --repo RunanywhereAI/runanywhere-sdks --workflow=release.yml --limit 5\n```\n\n**Do not block synchronously waiting on it.** Load the `Monitor` tool (or a background\npoll loop) to watch for completion/failure, and keep doing other release prep (changelog\nreview, CodeRabbit thread triage, branch-protection check) in the meantime. Keep\nnotifications to real state changes only — don't ping on every intermediate job\ncompleting, only on the run's overall failure or full completion.\n\n### REAL job durations — measured, not guessed. NEVER cancel a job for being \"slow\".\n\nMeasured on the 0.20.25 train (2026-08-21/22). A release train legitimately takes\n**4-5 hours** wall-clock when `native_web` has to build.\n\n| Job | Real duration | Notes |\n|---|---|---|\n| **`native_web`** (release.yml) | **~3h51m** | The long pole, by far. Its `Vendor ONNX Runtime and Sherpa-ONNX WASM archives` step alone is **~71 minutes** — it BUILDS onnxruntime + sherpa to WASM from source; it is not a git clone. Then 4 more WASM builds: CPU core+llamacpp+onnx ~89m, WebGPU llamacpp ~37m, WebGPU onnx+sherpa ~33m. |\n| `native_ios` | ~60-75 min | XCFrameworks + MLX. No longer compiles neurun — it downloads the pinned NeuRT archives (see §0b), which is seconds, not minutes. |\n| `swift-spm` (pr-build) | **69-85 min** | `Build XCFramework` step is most of it. Measured across 6 consecutive `main` runs (2026-08-21/22): 69, 78, 75, 85, 77, 69. A run sitting at ~78 minutes is **healthy**, not hung — check this row before reaching for cancel. |\n| `kotlin-android` (pr-build) | ~40 min | |\n| `python-windows` (pr-build) | ~40 min | |\n| `electron (windows-2022)` (electron-sdk-ci) | ~40 min | |\n| `rcli-windows` | ~30 min | |\n| `native_electron / native_win_arm64_qhexrt` | ~28 min | Self-hosted; may sit **queued** for a long time first if the single runner is busy. Queued ≠ hung. |\n| `native_electron / native_win_x64_and_package` | ~35-45 min | |\n| `wasm` (pr-build) | ~12 min | **Do NOT use this as a proxy for `native_web`.** Totally different scope. |\n\n**The mistake this table exists to prevent**: `pr-build.yml`'s `wasm` job takes ~12\nminutes, so a ~50-minute `native_web` \"vendor\" step looks hung. It is not. On the 0.20.25\ntrain this misread caused two healthy `native_web` jobs to be cancelled at 2h19m and\n~55m, wasting ~3 hours and two full candidate builds. Before concluding \"hung\", check\nstep-level progress and compare against THIS table:\n\n```bash\ngh run view <run-id> --repo RunanywhereAI/runanywhere-sdks --json jobs \\\n    --jq '.jobs[] | select(.name==\"native_web\") | {startedAt, steps: [.steps[] | select(.status==\"in_progress\") | .name]}'\n```\n\n### `reuse_native_web_run_id` is the correct way to avoid re-paying that ~4 hours\n\nIf a run needs redoing and **nothing under `bindings/web/wasm/`, `engines/`, or `core/`\nchanged** since a run whose `native_web` succeeded, reuse it instead of rebuilding:\n\n```bash\ngit diff --stat <good-run-sha> <current-sha>      # prove the scope first\ngh workflow run release.yml --ref release/<version> \\\n    -f version=<version> -f reuse_native_web_run_id=<good-run-id>\n```\n\n`native_web` then shows `skipped`, and `sdk_web` + `validate_consumer_web` still run and\npass against the reused artifact — which is also your proof the reuse was valid. Track the\nrun id that **originally built** it: that is the id you keep citing (see the chaining note\nin step 7).\n\n---\n\n## 3. Sync checksums once native_ios finishes\n\nPre-tag, `gh release download` will not work (there is no release yet) — pull the run's\nartifact directly:\n\n```bash\ngh run download <candidate-run-id> --repo RunanywhereAI/runanywhere-sdks \\\n    --name native-ios-macos --dir release-artifacts/native-ios-macos\n```\n\nVerify every `.zip` against its `.sha256` sidecar **yourself** before trusting it:\n\n```bash\ncd release-artifacts/native-ios-macos\nfor f in *.zip; do shasum -a 256 -c \"${f}.sha256\" || echo \"MISMATCH: $f\"; done\ncd -\n```\n\nThen sync:\n\n```bash\nbindings/swift/scripts/sync-checksums.sh release-artifacts/native-ios-macos\n```\n\n**KNOWN FACT — do not treat this as a bug**: `RunAnywhereMLXRuntime` (and occasionally\n`RACommons`) are **not byte-reproducible** between rebuilds of identical source. Expect\ntheir checksums to differ from any prior candidate build every single time you rebuild,\neven with zero source changes. Only investigate if a checksum for a framework that\nhistorically IS reproducible (e.g. plain XCFrameworks with no Swift codegen step)\nsuddenly changes.\n\n**Where each checksum actually lives — these are NOT all in one file:**\n\n| Manifest | Carries |\n|---|---|\n| root `Package.swift` | `RACommons` + the 5 backend XCFrameworks (6 checksums total) |\n| nested Flutter `Package.swift` (under `bindings/flutter/`) | same 6 |\n| `bindings/flutter/packages/runanywhere_mlx/ios/runanywhere_mlx.podspec` → `mlx_checksums` hash | `RunAnywhereMLXRuntime`, `RunAnywhereMLXMetal`, `RunAnywhereMLXResources` — **only here**, nowhere else |\n\nAfter `sync-checksums.sh` runs, verify centralization didn't drift and re-run the version\ncoherence gate locally:\n\n```bash\nbash scripts/validation/gates/check_flutter_centralization.sh\nbash scripts/validation/gates/check_release_version_coherence.sh\n```\n\nCommit and push the checksum-sync commit to the release branch:\n\n```bash\ngit add Package.swift bindings/flutter/Package.swift \\\n    bindings/flutter/packages/runanywhere_mlx/ios/runanywhere_mlx.podspec\ngit commit -m \"release: sync v<version> checksums from candidate run <candidate-run-id>\"\ngit push\n```\n\n### THIS STEP IS NOT OPTIONAL — skipping it fails `publish` after the tag already exists\n\n**What happens if you skip it** (observed on the 0.20.25 train): every build and package\njob goes green, `publish` gets all the way past flatten and the asset-manifest assertion,\nthen dies at **`Verify built Apple checksums match tagged package contracts`**:\n\n```\nmismatch: RACommonsBinary\n  Package.swift: 0609dae6...      release zip: 1ae55ae4...\nmismatch: RABackendLlamaCPPBinary\n  Package.swift: 0597442f...      release zip: 7062f7c8...\n>> Done. 8 processed, 0 missing, 4 failed.\nERROR: built Swift archives do not match the immutable tagged manifest\n```\n\nNote `8 processed / 4 failed` is really **2 distinct archives counted twice** (root\nmanifest + Flutter manifest each). On that train `RACommons` and `RABackendLLAMACPP`\ndrifted while ONNX/Sherpa/NeuRT/MLX/MLXMetal/MLXResources all matched — so \"6 of 8 passed\"\nis a normal-looking partial failure, not evidence of something exotic.\n\n**Recovery once the tag already exists** (this is safe and was done on 0.20.25):\n\n1. Confirm nothing was actually published — a failed `publish` dies *before* creating the\n   release, so there is usually nothing to undo:\n   ```bash\n   gh release view v<version> --repo RunanywhereAI/runanywhere-sdks   # expect \"release not found\"\n   ```\n2. Download the **exact artifacts the release will publish** (the candidate run named in\n   `publish_from_run_id`), verify each against its sidecar, and sync on a branch off `main`.\n3. Merge that branch, then **re-point the tag** at the new commit. `git push origin --delete`\n   can hang on flaky networks; the API is more reliable:\n   ```bash\n   gh api -X DELETE repos/RunanywhereAI/runanywhere-sdks/git/refs/tags/v<version>\n   gh api -X POST   repos/RunanywhereAI/runanywhere-sdks/git/refs \\\n       -f ref=\"refs/tags/v<version>\" -f sha=\"<new-main-sha>\"\n   ```\n   Only do this while **no release exists and nothing has been published**. If a release\n   was already published, do NOT move the tag — cut the next patch version instead.\n4. **Creating a tag via the API DOES trigger `release.yml`'s `push` trigger** (unlike a\n   `GITHUB_TOKEN` git-push of a tag, which does not). So immediately after re-tagging you\n   will have TWO runs: a full rebuild from the `push` event, and your own\n   `workflow_dispatch`. Identify by event and cancel the `push` one:\n   ```bash\n   gh run list --repo RunanywhereAI/runanywhere-sdks --workflow=release.yml --limit 3 \\\n       --json databaseId,event,headBranch --jq '.[] | \"\\(.databaseId) \\(.event) \\(.headBranch)\"'\n   gh api -X POST repos/RunanywhereAI/runanywhere-sdks/actions/runs/<push-run-id>/cancel\n   ```\n\nCheapest prevention: run `sync-checksums.sh --check <zip_dir>` against the candidate's\nartifacts and require **`N processed, 0 missing, 0 failed`** before you merge the release\nPR at all.\n\n**Recurred on v0.20.30 and again on v0.20.31** — same `mismatch: RACommonsBinary` /\n`mismatch: RACommons` failure, same root cause (step 2's candidate dispatch skipped\nentirely both times), fixed the same way both times: compute the real checksum from the\nalready-built release-artifact zip (`gh run download <failed-run-id> --name\nnative-ios-macos`), commit it on a fix branch, merge, then move the tag (§10d) to\nre-trigger `release.yml`. On v0.20.31 this ALSO surfaced the `RunAnywhereMLXRuntime`\npodspec entry as independently stale — `RAC_CHECKSUM_SKIP` means `publish` never checks\nit, so it would have shipped broken to Flutter consumers silently. **When fixing this\nfailure after the fact, check every row of the checksum-location table in step 3, not\nonly the archive named in the CI error** — the ones `RAC_CHECKSUM_SKIP` exempts are\nexactly the ones that won't tell you.\n\n---\n\n## 4. Watch for the \"package intentionally doesn't bundle a sibling dep\" bug class\n\nThis exact release hit a real bug in `bindings/web/scripts/package-sdk.sh`:\n`@runanywhere/web-onnx` deliberately does **not** bundle its own `proto-ts` copy (only\n`core`/`llamacpp` do — enforced by `bindings/web/scripts/validate_public_packages.py` and\nmirrored in `bindings/react-native/scripts/validate_public_packages.py`), but the\npackaging script's install-smoke-test (`verify_candidate_install`, around line 165 of\n`package-sdk.sh`) never supplied a resolution target for that unbundled dependency, so\n`npm install` reached out to the real npm registry for a not-yet-published version and\nfailed with `ETARGET`.\n\nFix pattern (already applied, keep it this way): the `onnx` call to\n`verify_candidate_install` passes the just-built local `proto-ts` tarball\n(`$PROTO_ARCHIVE`, built via `npm pack ../proto-ts`) as an explicit extra install arg —\nsee lines ~209-233 of `bindings/web/scripts/package-sdk.sh`, where `core` and `llamacpp`\ncalls omit it (they bundle proto-ts themselves) but `onnx`'s call appends `\"$PROTO_ARCHIVE\"`.\n\n**Generalize this as a standing check for every release**: grep for the asymmetric\nbundling flags before trusting any packaging script —\n\n```bash\ngrep -n \"expects_bundled_proto\\|--bundle\" \\\n    bindings/web/scripts/package-sdk.sh bindings/react-native/scripts/package-sdk.sh\n```\n\nWhenever a package intentionally does NOT bundle a sibling first-party dependency, its\nlocal pre-publish verification step must supply that dependency some other way (a local\ntarball) — the real registry version genuinely does not exist yet at candidate-build\ntime, and letting `npm`/`yarn` hit the real registry will `ETARGET`.\n\n**Before trusting any packaging script is bug-free, actually run it** against the real\ndownloaded native artifacts and check the exit code — do not just read the code and\nassume it's fine:\n\n```bash\ncd bindings/web && ./scripts/package-sdk.sh   # then check $?\ncd bindings/react-native && ./scripts/package-sdk.sh\n```\n\n### 4b. The two Electron-specific release bugs (both fixed — know the shapes)\n\nThe 0.20.25 train was the **first** release to run Electron packaging inside `release.yml`,\nand it surfaced two defects that no local test or green PR check could have caught. Both\nare fixed; recognize the shapes because they generalize.\n\n**(1) `sync-versions.sh` has a hardcoded package list, and new packages get missed.**\n`bindings/electron/packages/neurt/package.json` sat at the previous version while every\nsibling moved, because `neurt` was added (electron#734) after that list was last touched.\nSymptom: `native_win_x64_and_package` fails with\n\n```\nERROR: npm pack did not produce .../runanywhere-electron-neurt-<version>.tgz\n```\n\n— because `npm pack` correctly produced `...-neurt-<OLD-version>.tgz` from the stale\nmanifest. **Whenever you add a package anywhere, add it to `sync-versions.sh` the same\nday.** Cheap pre-flight check that catches the whole class:\n\n```bash\ngrep -h '\"version\"' bindings/electron/package.json bindings/electron/packages/*/package.json \\\n    bindings/electron/native/package.json | sort -u     # expect exactly ONE distinct version\n```\n\n**(2) `publish` downloads EVERY artifact, including intermediate hand-off bundles.**\n`publish`'s `Download all artifacts` step has no name filter. `electron-packaged-tarballs`\nis `native_win_x64_and_package`'s raw output whose only purpose is feeding `sdk_electron`,\nwhich re-uploads the curated public set as `sdk-electron`. Flattening both duplicated all\n5 Electron tarballs *and* dragged in the raw bundle's own `proto-ts` copy, which collided\nwith `sdk_web`'s:\n\n```\n::error::flatten collision: release-flat/runanywhere-proto-ts-<v>.tgz already exists\n::error::Refusing to publish — 6 filename collision(s) detected during flatten\n```\n\nFixed by pruning that directory in the flatten `find`\n(`-type d -name \"electron-packaged-tarballs\" -prune -o -type f \\( ... \\)`). **Standing\nrule: any new intermediate artifact must be either excluded from the flatten or named so\nit cannot collide.** When editing that `find`, test the exact expression against a fake\n`release-artifacts/` tree locally first — `-prune -o` + `-print0` is easy to get subtly\nwrong and the only other way to find out costs a full release run.\n\n---\n\n## 5. Resolve CodeRabbit (or any bot) review findings properly\n\nDo not resolve a review thread without a real, evidence-backed reply. Steps:\n\n1. Fetch unresolved threads via GraphQL:\n\n```bash\ngh api graphql -f query='\nquery($owner:String!, $repo:String!, $pr:Int!) {\n  repository(owner:$owner, name:$repo) {\n    pullRequest(number:$pr) {\n      reviewThreads(first:100) {\n        nodes { id isResolved comments(first:10) { nodes { id databaseId body } } }\n      }\n    }\n  }\n}' -f owner=RunanywhereAI -f repo=runanywhere-sdks -F pr=<pr-number>\n```\n\n2. For each unresolved thread, reply via REST with `in_reply_to` set to the comment's\n   `databaseId` (not the GraphQL node id), citing concrete evidence — a real run id,\n   a real checksum, a real commit SHA, not \"looks fine\":\n\n```bash\ngh api -X POST repos/RunanywhereAI/runanywhere-sdks/pulls/<pr-number>/comments \\\n    -f body=\"Verified against candidate run <run-id>: RunAnywhereMLXRuntime checksum\nmatches release-artifacts/native-ios-macos/RunAnywhereMLXRuntime-ios-v<version>.zip.sha256.\nSee commit <sha>.\" \\\n    -F in_reply_to=<comment-database-id>\n```\n\n3. Only then resolve, via GraphQL mutation on the thread id from step 1:\n\n```bash\ngh api graphql -f query='\nmutation($threadId:ID!) { resolveReviewThread(input:{threadId:$threadId}) { thread { isResolved } } }\n' -f threadId=<thread-node-id>\n```\n\n---\n\n## 6. Merge\n\nBefore merging, verify:\n\n```bash\ngh pr view <pr-number> --repo RunanywhereAI/runanywhere-sdks --json mergeable,reviewDecision\n```\n\nZero unresolved review threads (re-check the GraphQL query from step 5 — it should return\nnone with `isResolved: false`), and `mergeable == \"MERGEABLE\"`.\n\nConfirm bypass eligibility before relying on `--admin`:\n\n```bash\ngh api repos/RunanywhereAI/runanywhere-sdks/branches/main/protection \\\n    --jq '.required_pull_request_reviews.bypass_pull_request_allowances'\ngh api user --jq .login\n```\n\nIf your login appears in that allowance list (users, or a team/app you belong to):\n\n```bash\ngh pr merge <pr-number> --repo RunanywhereAI/runanywhere-sdks --squash --admin\n```\n\nIf not on the list, **stop** — ask a human to approve or merge instead of forcing it.\n`--admin` is a legitimate configured bypass only for allow-listed identities; do not use\nit to route around review for anyone else.\n\n---\n\n## 7. GOTCHA — cancel the auto-tag-dispatched rebuild, dispatch a publish-only run instead\n\nThe moment the PR merges, `auto-tag.yml` runs, pushes the `v<version>` tag, and **itself**\ndispatches a brand-new full `release.yml` run at that tag (see orientation section — plain\ntag pushes via `GITHUB_TOKEN` don't auto-trigger, so the workflow does it explicitly).\nThis is redundant — you already have a fully green, checksum-verified candidate from step\n2-3 — and wasteful, because Apple/MLX bytes will drift again for zero benefit (see the\nnon-reproducibility fact in step 3).\n\n**Cancel it immediately, before it burns real compute** (ideally within the first minute\nor two, before native_ios starts real work):\n\n```bash\ngh run list --repo RunanywhereAI/runanywhere-sdks --workflow=release.yml --limit 5\ngh api -X POST repos/RunanywhereAI/runanywhere-sdks/actions/runs/<auto-triggered-run-id>/cancel\n```\n\nThen dispatch a **publish-only** run that reuses your already-verified candidate's exact\nbytes instead of rebuilding:\n\n```bash\ngh workflow run release.yml --ref v<version> \\\n    -f version=<version> \\\n    -f publish_from_run_id=<candidate-run-id>\n```\n\n**CRITICAL SUB-GOTCHA**: if your candidate run in step 2 itself passed\n`reuse_native_web_run_id=<X>` to skip rebuilding WASM (a packaging-only re-release), that\ncandidate run **never uploaded its own copy of the `native-web` artifact** — it was\nskipped. `publish_from_run_id` alone will then fail during the release-manifest assertion\nwith `\"Missing or empty Web WASM payload\"`, because the publish job's artifact-download\nstep only pulls `reuse_native_web_run_id`'s artifact when that input is passed to *this*\ndispatch too (see `.github/workflows/release.yml` around the \"Download reused Web natives\"\nstep — it is gated on `inputs.reuse_native_web_run_id != ''`, independent of\n`publish_from_run_id`). You must **also** pass the same original run id:\n\n```bash\ngh workflow run release.yml --ref v<version> \\\n    -f version=<version> \\\n    -f publish_from_run_id=<candidate-run-id> \\\n    -f reuse_native_web_run_id=<X>\n```\n\nThis chains forward across releases: whichever run **originally** built `native_web` is\nthe run you keep citing at every subsequent `reuse_native_web_run_id`, not the most recent\nrun that merely borrowed it from an earlier one. Track that original run id explicitly if\nyou're doing back-to-back packaging-only releases.\n\n---\n\n## 8. Verify the draft release's asset set\n\nThe publish job only creates a **draft** GitHub Release (`softprops/action-gh-release`,\n`draft: true`) with built assets attached — it does not touch npm/Maven/pub.dev at all\n(grep `release.yml` for `npm publish`, `mavenCentral`, `pub publish` — none exist there).\n\nExpected asset count for a full, non-Windows-focused release: **~65 assets** —\n\n- 9 Apple archives (iOS/macOS XCFrameworks: RACommons + 5 backends + MLX runtime/Metal/\n  resources + CoreML/NeuRT)\n- 3 Android RACommons zips (arm64-v8a, armeabi-v7a, x86_64)\n- 1 Linux RACommons tar.gz\n- 1 Windows RACommons zip (preserved as an asset, advisory — see step 9)\n- 1 Web WASM tar.gz (`RACommons-web-v<version>.tar.gz`)\n- 3 rcli tarballs (macOS arm64, Linux x86_64, Windows x86_64 — macOS may also have a\n  notarized `.dmg` if Developer ID/notary secrets are configured; this is soft-skipped\n  otherwise, not a failure)\n- 1 Kotlin Maven repo zip\n- 8 npm tarballs (proto-ts, web core, web-llamacpp, web-onnx, RN core, RN llamacpp, RN mlx,\n  RN onnx)\n- 5 Electron npm tarballs (`runanywhere-electron-<v>.tgz` core, `-llamacpp`, `-onnx`,\n  `-sherpa`, `-neurt`) — added by `sdk_electron`; each has its own `.sha256` sidecar\n  counted separately below. **`electron-qhexrt` is deliberately excluded** — it still\n  gets built and packaged, but only as the `sdk-electron-qhexrt-private` workflow\n  artifact (not a release asset); download it separately for a manual `npm publish`.\n- 1 `MANIFEST.txt`\n- **explicitly ZERO** `qhexrt`-named files (the manifest assertion step hard-fails the run\n  if it finds any — confirm the run actually reached the publish job and didn't die there)\n\nEvery asset above except `MANIFEST.txt` has a `.sha256` sidecar, which is itself a\nseparate asset. **v0.20.25 landed at exactly 65 assets** — use that as the concrete\nbaseline rather than re-deriving the arithmetic.\n\nCross-check every checksum's `.sha256` sidecar against what got committed to\n`Package.swift` / the flutter `Package.swift` / the MLX podspec in step 3 — they must\nmatch byte for byte, because this run's artifacts are exactly what step 10 (sdk-publish)\nships to real consumers.\n\n```bash\ngh run view <publish-run-id> --repo RunanywhereAI/runanywhere-sdks --json jobs \\\n    --jq '.jobs[] | {name, conclusion}'\n\n# Asset count + the Electron set + the qhexrt-leak assertion, in three commands:\ngh release view v<version> --repo RunanywhereAI/runanywhere-sdks --json assets --jq '.assets|length'\ngh release view v<version> --repo RunanywhereAI/runanywhere-sdks --json assets \\\n    --jq '[.assets[].name | select(startswith(\"runanywhere-electron\"))] | sort'\ngh release view v<version> --repo RunanywhereAI/runanywhere-sdks --json assets \\\n    --jq '[.assets[].name | select(test(\"qhexrt\";\"i\"))]'          # MUST be []\n```\n\n### Prove the artifacts are real — don't trust green checkmarks\n\nA green `publish` only proves the assertions ran. To prove the bytes are right, run the\nrepo's own fail-closed gate against the **published** tarballs. This is the single highest\n-value verification in the whole runbook and it takes about a minute:\n\n```bash\ngh release download v<version> --repo RunanywhereAI/runanywhere-sdks \\\n    -p 'runanywhere-electron*.tgz' -D /tmp/verify\npython3 scripts/validation/gates/check_plugin_natives.py --expect-version <version> \\\n    $(for t in /tmp/verify/*.tgz; do printf -- '--tarball %s ' \"$t\"; done)\n```\n\nExpect `All N check(s) passed: plugin natives are routable and no staging-URL placeholder\nwas found.` On v0.20.25 that was **20 checks** across 5 tarballs, asserting three separate\nthings per binary: the real `STAGING_BASE_URL` is baked in (no `YOUR_STAGING_BASE_URL`\nplaceholder), `rac_commons` embeds the right version string, and each backend is actually\n**routable** (real op tables — `g_llamacpp_ops`, `g_sherpa_stt_ops`, `g_neurt_llm_ops`,\netc.), which is what distinguishes a real plugin from a same-sized shell.\n\nSanity-check the shipped layout too — a tarball missing a platform is a silent regression:\n\n| Package | Expected `prebuilds/` |\n|---|---|\n| `@runanywhere/electron` | `darwin-arm64` + `win32-x64` + `win32-arm64` |\n| `-llamacpp`, `-onnx`, `-sherpa` | `darwin-arm64` + `win32-x64` |\n| `-neurt` | `darwin-arm64` only (Apple Neural Engine) |\n| `-qhexrt` (private artifact) | `win32-arm64` only (Hexagon NPU) |\n\n---\n\n## 9. Windows handling — build/preserve only, never auto-publish\n\n`native_windows` and `native_rcli_windows` run and their outputs are **preserved** as\nrelease assets (proving the binaries build), but nothing Windows-specific is published to\nany package registry as part of the standard flow unless a human explicitly asks for it —\nthat's a deliberate, separate decision, not an oversight.\n\nFor anything that needs actual execution on Windows (smoke-testing the native Windows\nbuild, or Electron's Windows packaging), do not assume a macOS cross-build proves\nWindows correctness — SSH to the real Windows box. The team keeps a **Windows ARM64\nmachine reachable over SSH**; its hostname, user, key, and mesh-VPN node name are\noperator-specific and deliberately not recorded in this repo — read the Windows/ARM64\nhost entry out of the operator's `~/.ssh/config` and use that alias. If there is no such\nentry, the box is not configured for you: say so and ask, rather than guessing.\n\n```bash\nWIN_BOX=<the ssh alias from ~/.ssh/config>\n# Ask the mesh-VPN client whether the node is up FIRST (e.g. `tailscale status`,\n# matching on that host entry's HostName) — never just try SSH and retry on timeout.\nssh -o ConnectTimeout=8 \"$WIN_BOX\" \"whoami\"\n```\n\nIf the VPN reports the node offline/asleep, **that's not a config problem** — ask the\nuser to wake the box; don't debug SSH config against a machine that's simply off.\n\n---\n\n## 10. Publish the draft, then hand off\n\nOnce the draft's asset set is verified complete and correct (step 8):\n\n```bash\ngh release edit v<version> --repo RunanywhereAI/runanywhere-sdks --draft=false --latest\n```\n\nThis is the point where SwiftPM's `from: \"<version>\"` resolution and the download URLs\nbaked into `Package.swift` start actually working for real external consumers — draft\nrelease assets are not publicly downloadable, so nothing before this line is\nexternally live.\n\n**Hand off to the `sdk-publish` skill** for the actual npm / Maven Central / pub.dev /\nSwiftPM-distribution-repo publishing steps. This skill's job ends here.\n\n---\n\n## 10b. MANDATORY, and it turns `main` RED until you do it — cut runanywhere-swift\n\nThe moment `v<version>` exists in this repo, `check_swift_dist_repo_sync.sh` (run by\n`pr-build.yml`'s `centralization` job) **fails on every PR and every push to `main`**:\n\n```\n[FAIL] runanywhere-swift has no 0.20.25 tag (latest: 0.20.24)\n       v0.20.25 is published here, so 'from: \"0.20.25\"' is broken for every\n       SwiftPM consumer of https://github.com/RunanywhereAI/runanywhere-swift.git\n```\n\nThis is **by design**, not a bug, and it is the #1 reason a release looks \"not green\"\nafter everything else succeeded. Do not go hunting for a code defect — cut the repo:\n\n```bash\ngit clone https://github.com/RunanywhereAI/runanywhere-swift.git /tmp/ra-swift\nbindings/swift/scripts/sync-dist-repo.sh --zips <zip_dir> --tag /tmp/ra-swift\n# verify BEFORE pushing (content check passes locally; the tag check needs the push)\nRUNANYWHERE_SWIFT_DIST_REPO=/tmp/ra-swift \\\n    bash scripts/validation/gates/check_swift_dist_repo_sync.sh\ngit -C /tmp/ra-swift push origin main --follow-tags\nbash scripts/validation/gates/check_swift_dist_repo_sync.sh    # now [OK]\n```\n\nGotchas found doing this on 0.20.25:\n\n- `--check` **cannot be combined** with `--zips`/`--commit`/`--tag` (hard error). Run a\n  bare `--check <repo>` first to preview, then the real invocation separately.\n- `--check` legitimately reports `ERROR: distribution repo is out of sync` before you\n  sync — that's the preview telling you what it will change (expect the `sdkVersion` bump,\n  ~3 changed files, `0 added / 0 removed`). Non-zero exit there is not a failure.\n- The script **never pushes**; `--tag` only creates the tag locally. The dist repo tag has\n  **no `v` prefix** (`0.20.25`, not `v0.20.25`) while the monorepo tag does.\n- Running the gate locally right after `--tag` still shows `[FAIL] ... has no <version>\n  tag` while simultaneously showing `[OK] ... checkout matches: all 6 checksums`. That is\n  expected: the content check reads your local checkout, the tag check queries the real\n  remote. It clears the moment you push.\n- It chains into `sync-checksums.sh`, so run it **after** step 3's checksums are committed,\n  or the dist manifest gets stale hashes.\n\n---\n\n## 10c. Windows + self-hosted-runner traps (read before debugging a red Windows job)\n\nThese all cost a full CI cycle. The unifying fact:\n\n> The self-hosted runner service executes as `NT AUTHORITY\\NETWORK SERVICE`, which has its\n> own PATH **and** its own NTFS permissions. Anything you verify over SSH on that box tells\n> you nothing about what the job can do. `native_win_arm64_qhexrt` already carries a long\n> comment about this — read it before inventing a fix, and keep it in sync with neurun's\n> equivalent lane, which runs on the *same machine*.\n\n**Environment**\n\n- **`bash: command not found`** — Git for Windows puts `Git\\cmd` (git.exe) on PATH but not\n  `Git\\bin` (bash.exe). `actions/checkout` still works because it calls git.exe directly,\n  which makes this look unrelated to runner setup.\n- **`pwsh` does not exist on that box** — Windows PowerShell 5.1 only. Use\n  `shell: powershell`.\n- **`gh` is not installed on that box, and must not be assumed anywhere.** A QHexRT\n  download died on `gh: command not found`. The fix is deliberately *not* to install it:\n  every fetch of a private release asset now goes through\n  `fetch_release_asset()` in `scripts/build/_release_asset.sh`, which uses `curl` + the\n  REST API. `curl` ships with Windows, macOS and every Linux image we use, so this is one\n  code path everywhere instead of a per-runner provisioning step. If you add a new\n  consumer of a private asset, source that helper — do not reach for `gh`.\n  Note the helper matches the asset name **exactly**: a prefix match picks the `.sha256`\n  sidecar (its name starts with the tarball's), and you get a 65-byte \"archive\" that fails\n  much later with a confusing error.\n- **Tool availability is the single most common failure class on this runner** — `bash`,\n  `python3`, `ninja`, `gh`, GNU `tar` and NDK `.exe` suffixes have each broken a lane\n  once. When a self-hosted job fails, check \"does this tool exist as spelled\" before\n  reading any logic.\n- **`ln -s` under Git-Bash COPIES the directory** unless `MSYS=winsymlinks:nativestrict`.\n  It does not fail, so the result looks like real content and whatever validates a\n  \"must be a symlink\" invariant rejects a payload that was actually fine. Never use shell\n  `ln -s` on this box — go through `scripts/build/_selection.py`.\n- **Windows selects with a JUNCTION, not a symlink, and that is deliberate.** The runner\n  service account cannot create symlinks (below), but a junction is the same kind of\n  reparse point to a directory and needs no privilege. Two traps when reading that code:\n  `Path.is_symlink()` returns **False** for a junction (`stat.S_ISLNK` is true only for\n  `IO_REPARSE_TAG_SYMLINK`) — use `_selection.is_selection()`; and a junction stores an\n  **absolute** t\n\n<!-- File: .claude/skills/sdk-test-starters/SKILL.md (Tokens: ~7370 | Category: anthropic-skill) -->\n---\nname: sdk-test-starters\ndescription: Run a REAL end-to-end inference smoke test (model download + load + generate, with visible output) on every RunAnywhere starter app — iOS, macOS/Swift, Android, Web, Electron — after a runanywhere-sdks release. Build success alone is never sufficient evidence.\n---\n# SDK Test Starters\n\nPost-release validation runbook for the five RunAnywhere SDK consumer apps. This\nskill exists because \"it built\" has been reported as \"tested\" before, and that is\nnot evidence of anything except that the compiler ran.\n\n## 0. The evidence bar (applies to every app, no exceptions)\n\nFor each app you must produce, and be able to state explicitly:\n\n1. **Version proof** — print/log the actual resolved SDK version from inside the\n   running app (a startup log line, a version screen, an `swift package show-dependencies`\n   / `./gradlew dependencies` / `npm ls @runanywhere/...` output, etc). Never assume\n   a version bump \"took\" just because you edited a manifest.\n2. **Real model acquired** — a real (small) model downloaded fresh, or confirmed\n   already cached, by the app itself (not a file you hand-placed unless that IS the\n   app's supported flow).\n3. **Load** — the model actually loads (watch for a load-succeeded log/state, not\n   just \"no crash\").\n4. **One real inference call** — LLM `generate()` is almost always the easiest\n   modality to drive; use it unless another modality is genuinely less work for\n   that specific app.\n5. **Visible output** — capture the actual generated text (or audio/image) and\n   quote it in your report. \"It responded\" is not enough — show what it said.\n6. **Logs reviewed** — read the console/log stream for errors and warnings and\n   explicitly explain any that appear. Do not silently drop a stack trace because\n   the app \"still produced output.\"\n\nReporting an app as tested from a build/compile success alone is a skill violation.\nIf you could not get past build, that is a BLOCKED report (see §8), not a PASS.\n\n## 1. Locating each app's real local checkout\n\nThis machine accumulates many differently-named copies of the same repos over time\n(worktrees, experiment clones, old branches, scratch dirs). **Never trust a\ndirectory name.** Before touching anything in a candidate directory:\n\n```bash\ngit remote get-url origin          # must be https://github.com/RunanywhereAI/<repo>.git\ngit status --short --branch        # what branch, what's dirty, is it someone else's WIP?\n```\n\nAs of this writing, the known-good local checkouts for this user are here (verify\nthis each time — do not hardcode it further than \"check this location first\"):\n\n```\n~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-ios       -> RunanywhereAI/runanywhere-ios.git\n~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-android   -> RunanywhereAI/runanywhere-android.git\n~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-web       -> RunanywhereAI/runanywhere-web.git\n~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-electron  -> RunanywhereAI/runanywhere-electron.git\n```\n\nThe macOS/Swift case has no separate consumer repo — see §4, it lives inside\n`~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks` (confirm with\n`git remote get-url origin` there too — this machine also has stray full-monorepo\nclones elsewhere, e.g. under `~/development/hard/ambient/`, that are NOT the one to\nuse unless you've verified it's the intended checkout).\n\nThese starter checkouts are commonly sitting on a feature branch, not `main` — e.g.\nat last check `runanywhere-ios` and `runanywhere-android` were on\n`add-gemma4-qwen3.6-qwen3.8-models` and `runanywhere-web` on `add-supertonic-3-tts`.\nThat is fine to test FROM, but say so in your report, and do not `git checkout` away\nfrom a branch that has uncommitted work you didn't create — if `git status --short`\nshows dirty files you don't recognize, stop and ask rather than stashing/resetting.\n\nIf genuinely unsure which directory is real, `git clone` a fresh one into the\nscratchpad rather than guessing — a wrong guess wastes far more time than a clone.\n\n## 2. Cross-repo facts this skill assumes\n\n- SDK version SoT: `core/VERSION` in `runanywhere-sdks`, bumped everywhere by\n  `scripts/release/sync-versions.sh <version>`.\n- Release publish is `.github/workflows/release.yml` (dispatch or `v*.*.*` tag).\n- **Never** treat a build success against `RUNANYWHERE_USE_LOCAL_NATIVES=1` (or any\n  monorepo-local staged artifact) as proof the *public* release works — see §4.\n- QHexRT/QNN material must never appear in a public artifact — release.yml\n  hard-fails on it. If anything you're testing surfaces a QHexRT asset in a public\n  package, stop and flag it, don't route around it.\n\n## 3. iOS — `runanywhere-ios`\n\nSDK pin lives in `Package.swift`:\n\n```swift\n.package(url: \"https://github.com/RunanywhereAI/runanywhere-swift.git\", from: \"0.20.19\")\n```\n\nThis only resolves once `runanywhere-swift` (the generated Swift-only SPM\ndistribution repo — see `~/.claude/skills/sdk-publish/SKILL.md` §4) is\nactually tagged at the target version. Steps:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-ios\n# bump the `from:` version in Package.swift to the target release, then:\nswift package update            # or open in Xcode and let it resolve\n./scripts/build_and_run_ios_sample.sh simulator \"iPhone 16 Pro\"\n```\n\n`./scripts/verify.sh` is the CI-equivalent gate: it resolves the package remotely\nand runs a full `xcodebuild` against the iOS Simulator — use it to prove the\nresolve is clean (it fails if `Package.resolved` ends up dirty, i.e. the committed\npin was stale). `./scripts/smoke.sh` is a fast, non-compiling grep-based check —\nit does NOT count as evidence for this skill's bar, only `build_and_run` +\nwatching real output does.\n\nDrive one real `generate()` call through the app UI (or the equivalent debug\naction if the sample exposes one) against a small downloaded model, then watch the\nruntime log stream for the version line, the model download/load, the generation,\nand any errors:\n\n```bash\nlog stream --predicate 'subsystem CONTAINS \"com.runanywhere\"' --info --debug\n```\n\nMost loggers use `com.runanywhere.RunAnywhereAI`; a few use plain\n`com.runanywhere`; the SDK logs under its own subsystem — the `CONTAINS` match\ncovers all of them. On a physical device use `idevicesyslog | grep \"com.runanywhere\"`\ninstead.\n\n## 4. macOS/Swift — no separate repo, use the in-tree example or rcli\n\nThere is no `runanywhere-macos` consumer repo. Two in-monorepo options, both inside\n`~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks`:\n\n- `bindings/swift/example/` — a minimal SwiftPM example app/executable.\n- `rcli/` — the C++ core's CLI, `rac_*` C ABI consumer.\n\n**The critical gotcha**: the repo's own `Package.swift` is fail-closed toward the\n*real published release* — it resolves the remote `binaryTargets` unless you\nexplicitly opt into locally staged XCFrameworks with\n`RUNANYWHERE_USE_LOCAL_NATIVES=1`. Every doc/script example in this repo sets that\nenv var (because local dev wants fast iteration against in-progress binaries) —\nbut that means copy-pasting those examples verbatim tests your LOCAL staged build,\nnot the actual public release artifact. To prove the published release works:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks/bindings/swift/example\n# DO NOT set RUNANYWHERE_USE_LOCAL_NATIVES — leave it unset so Package.swift\n# resolves the real remote release binaries for the target version.\nswift build\nswift run runanywhere-minimal \"Name three colours.\"\n```\n\nIf the example was previously built/run with the env var set, `swift build` may\nreuse a stale resolve — run `swift package reset` (or delete `.build/`) first if\nyou're not sure which mode the last resolve used.\n\nFor rcli instead (also validates the real published core):\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks\nrcli models download qwen3          # ~639MB, small enough for a quick smoke test\nrcli llm generate --model qwen3 \"Reply with exactly: RCLI WORKS\" --reasoning off\n```\n\n`rcli models list --all` shows the rest of the built-in catalog\n(`whisper-tiny`, `smolvlm2`, `piper`, …) if LLM generate isn't the right modality\nfor what you're verifying.\n\n## 5. Android — `runanywhere-android`\n\nVersion pin: `gradle/libs.versions.toml` has ONE shared catalog entry:\n\n```toml\nrunanywhere = \"0.20.19\"\n...\nrunanywhere-sdk      = { group = \"io.github.sanchitmonga22\", name = \"runanywhere-sdk\",              version.ref = \"runanywhere\" }\nrunanywhere-llamacpp = { group = \"io.github.sanchitmonga22\", name = \"runanywhere-llamacpp\",          version.ref = \"runanywhere\" }\nrunanywhere-onnx     = { group = \"io.github.sanchitmonga22\", name = \"runanywhere-onnx\",               version.ref = \"runanywhere\" }\nrunanywhere-qhexrt   = { group = \"io.github.sanchitmonga22\", name = \"runanywhere-qhexrt-android\",     version.ref = \"runanywhere\" }\n```\n\n**CRITICAL — do not bump this block as one unit.** `runanywhere-qhexrt-android` is\npermanently frozen at whatever version it was last published at before being\nexcluded from the public Maven train; by policy it can never move forward via\npublic Maven again. Verify the actual frozen version FRESH every time (do not\ntrust a remembered number — it doesn't change, but don't skip the check):\n\n```bash\ncurl -s https://repo1.maven.org/maven2/io/github/sanchitmonga22/runanywhere-qhexrt-android/maven-metadata.xml\n```\n\nThe other three artifacts (`runanywhere-sdk`, `-llamacpp`, `-onnx`) CAN and should\nmove to the new release version. This means the single `runanywhere` catalog ref\nmust be **split into two entries** — one at the new version for sdk/llamacpp/onnx,\none pinned to the frozen qhexrt version — not bumped as a block.\n\n**Before trusting a mixed pin works**: verify `RAC_PLUGIN_API_VERSION` (the\nplugin ABI version constant in the C++ core, exposed identically to Kotlin) is\nunchanged between the old public SDK version and the new one. If it changed, mixing\na newer sdk/llamacpp/onnx with the frozen qhexrt version is NOT safe — this is a\nhuman decision (needs either an ABI-compat shim or dropping qhexrt from this build),\nnot something to force through.\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-android\n./gradlew :app:assembleDebug\n./gradlew :app:installDebug        # installs to a connected emulator/device\nadb logcat | grep -i runanywhere   # or a more specific tag if the app uses one\n```\n\nLaunch the app, drive one real `generate()` call against a small downloaded model\n(the app's own model picker/download flow), confirm the resolved SDK version is\nlogged, and review logcat for errors before reporting a PASS.\n\n## 6. Web — `runanywhere-web`\n\n`package.json` has 4 `@runanywhere/*` deps pinned with a caret range:\n\n```json\n\"@runanywhere/proto-ts\":    \"^0.20.19\",\n\"@runanywhere/web\":         \"^0.20.19\",\n\"@runanywhere/web-llamacpp\":\"^0.20.19\",\n\"@runanywhere/web-onnx\":    \"^0.20.19\"\n```\n\nBump all 4 to the new version, then:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-web\nnpm install\nnpm run build\nnpm run dev      # vite --host localhost --port 3000 --strictPort\n```\n\nThe dev/preview server is what supplies COOP/COEP headers required for\n`SharedArrayBuffer` — WASM inference will silently fail (or refuse to init\nthreads) without them, so don't swap in a bare static file server.\n\n`~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks/bindings/web/AGENTS.md`\nhas the authoritative Validation section (its own heading is `## Validation`) —\nfull checklist:\n\n1. Fresh browser context.\n2. Example app served with COOP/COEP headers.\n3. Model download.\n4. Model load.\n5. Real browser inference for the target modality.\n6. Logs/screenshots reviewed.\n\nIts \"Required release gates\" (run before browser validation) from the same repo:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/runanywhere-sdks/bindings/web\nnpm run typecheck && npm run lint && npm run test && npm run build\nnpm run test:browser\nnpm run test:browser:release\ncd example && npm run typecheck && npm run build\n```\n\nStatic gates passing is not the smoke test — you still need an actual browser\nwatching real inference happen against the starter app. Prefer a real browser\nautomation tool (`claude-in-chrome` MCP tools, or Playwright — the maintained\nsuites under `bindings/web/tests/browser/` already use `playwright.config.ts`)\nover curling the served page: WASM inference genuinely needs a browser JS engine.\nLoad the served starter app, download/load a small model, trigger generate, and\nread the actual rendered output text plus the browser console — do not infer\nsuccess from an HTTP 200 on the page load.\n\n## 7. Electron — `runanywhere-electron` (SPECIAL CASE)\n\n**Do not assume a fresh SDK publish is available for this app.** Electron's own\nnpm packages (`@runanywhere/electron`, `-llamacpp`, `-onnx`, `-sherpa`, `-qhexrt`,\n`-neurt`) are on a SEPARATE, manual-publish pipeline — NOT part of `release.yml`\n(grep confirms zero mentions of these package names there). This separation is a\n**confirmed, deliberate** repo-owner decision, not a gap. Check\n`~/.claude/skills/sdk-publish/SKILL.md` §6 before assuming anything moved.\nElectron is **no longer WIP on a branch** — the old \"in-progress on\n`smonga/electron_upgrade`\" note is gone from the root `AGENTS.md`/`CLAUDE.md`\nentirely as of this checkout; `bindings/electron/` is a fully merged SDK on `main`.\nAs of `fix/electron-cicd-staging-url-npu-ane`,\n`.github/workflows/electron-native-package.yml` builds every backend for real and\npackages every tarball — but still does not publish; verify against a live run\nrather than assuming build automation status either way.\n\nTest the starter against whatever version it is CURRENTLY pinned to unless a human\nhas explicitly confirmed a newer Electron publish is ready and intended:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-electron\ncat package.json | grep '@runanywhere/electron'   # confirm current pin before touching it\n```\n\n**`@runanywhere/electron-qhexrt` being public on npm is a CONFIRMED, settled\npolicy decision** (repo owner, 2026-08-20), not a contradiction to flag — it\ncontains real Hexagon NPU binaries (`libQnnHtpV81Skel.so`, `QnnHtp*.dll`,\n`rac_commons.dll`, `runanywhere_qhexrt.dll`) on purpose. The QHexRT-stays-private\npolicy enforced elsewhere in this repo family is specifically about the\nGitHub-Release-side asset set (Android etc.), a different channel. Only re-raise\nthis if you find evidence the policy changed.\n\nTwo layers to actually exercise, do not conflate them:\n\n**(a) Cross-platform Electron JS + macOS native addon** — buildable and testable\nlocally on this Mac right now:\n\n```bash\ncd ~/development/ODLM/MONOREPOOO/Qualcomm/starters/runanywhere-electron\nnpm install\nnpm run build   # or the app's documented dev/start script — check package.json scripts\nnpm start        # or equivalent — launch the actual Electron app\n```\n\nDrive one real `generate()` call in the running app, watch its console/log output\nfor the resolved SDK version, model download, load, and generation, and read any\nerrors rather than ignoring them.\n\n**(b) Genuine Windows-native behavior** — a macOS build proves NOTHING here\nregardless of how clean the cross-platform code looks; the native addon and any\nWindows-only DLLs must be exercised on real Windows.\n\nThe team keeps a **Windows ARM64 test machine reachable over SSH**. Its hostname,\nuser, key, and VPN/mesh node name are operator-specific and deliberately not\nrecorded in this repo — read them out of the operator's own `~/.ssh/config` (look\nfor the Windows/ARM64 host entry) and use that alias below wherever `$WIN_BOX`\nappears. If no such entry exists, the box is not configured for you: say so and\nask, rather than guessing at a hostname.\n\n```bash\nWIN_BOX=<the ssh alias from ~/.ssh/config>\n```\n\n**Always check reachability first, never just try SSH and retry on timeout.** The\nbox lives on a private mesh VPN and is frequently asleep, so start by asking the\nVPN client whether the node is up (e.g. `tailscale status`, matching on the host\nentry's `HostName`) rather than opening a connection.\n\nIf the node reports `offline, last seen ...`, the box is asleep/powered off.\n**A connection timeout here means \"device asleep,\" not \"config is wrong.\"** Tell\nthe human and ask them to wake it; do not loop retrying SSH. Only once the node\nshows a live/idle state, confirm with:\n\n```bash\nssh -o ConnectTimeout=8 \"$WIN_BOX\" \"whoami\"\n```\n\nThen, with the repo already cloned there:\n\n```bash\nssh \"$WIN_BOX\" \"cd <repo-path-on-that-box>; git pull; npm install; npm run build\"\n# then run whatever launches the Windows Electron build and drive a real generate call\nscp \"$WIN_BOX\":<remote-log-or-artifact-path> ./local-copy   # pull back evidence to inspect\n```\n\nDo not claim Windows-specific validation happened unless you actually obtained a\nshell on that box and ran something real there — a clean macOS build is not\nsubstitute evidence.\n\n**`node_modules/electron` on this box silently tracks whichever arch last ran `npm\ninstall`, not which Node launches the test.** The x64 (llamacpp/onnx/sherpa) and\narm64 (QHexRT) lanes share ONE `runanywhere-electron` checkout/`node_modules` tree.\nPrepending an x64 Node to `PATH` and running `npx playwright test` does NOT make\nElectron itself x64 — `require('electron')` always resolves the single installed\n`node_modules/electron/dist/electron.exe`, and whichever arch that binary actually\nis determines which `prebuilds/win32-<arch>/` directory the running app looks in,\nregardless of `scripts/use-sdk.mjs local`'s staging (which itself is arch-aware\nbased on the *launching* Node, so it can correctly report \"staged win32-x64\" while\nthe process that will actually run is arm64 — a silent mismatch). Verify with:\n`powershell -Command \"$p=[IO.File]::ReadAllBytes('...\\electron.exe'); $peOff=\n[BitConverter]::ToInt32($p,60); [BitConverter]::ToUInt16($p,$peOff+4)\"` — `43620`\n(`0xAA64`) is ARM64, `34404` (`0x8664`) is x64. If it's the wrong arch (e.g. a prior\nsession's `npm install` for the other lane silently overwrote it), a genuine re-test\nof the other lane needs a FULLY SEPARATE checkout/`node_modules` tree installed\nunder the matching-arch Node — don't assume PATH-prepending alone is sufficient, and\ndon't trust `use-sdk.mjs status`'s \"platform: win32-x64\" output as proof the running\nElectron process is actually x64.\n\n**NPU hardware is a scarce, single-owner resource — serialize, never parallelize.**\nThis box has ONE physical Hexagon NPU. `engines/qhexrt/qhexrt_session.cpp`'s\n`session_open()` has a load-bearing comment on this exact failure mode: the HTP\nexposes a small number of protection domains with a per-PD context ceiling, so a\n*second* concurrent model load fails with `RAC_ERROR_BACKEND_INIT_FAILED`\n(\"Backend initialization failed\") — a fast (tens-of-ms), generic-looking failure\nthat is easy to mistake for a real regression. Confirmed once tonight: two agents\neach independently exercising QHexRT on this box at the same time produced exactly\nthis failure. Before concluding a QHexRT test genuinely failed, confirm nothing\nelse (another agent, a leftover Electron process, a stuck previous test run) is\nalso holding an NPU session open on this box — check for lingering `electron.exe`/\n`node.exe` processes from a prior run and kill them if orphaned, and never launch\ntwo NPU-touching verification passes concurrently on this machine.\n\n**\"Backend initialization failed\" has (at least) three distinct root causes —\ndon't stop at PD exhaustion.** Confirmed on 2026-08-17 after PD-exhaustion and\nstaleness were both ruled out (fresh rebuild, source-identical prebuilt, no other\nprocess holding a session): the *actual* bug was two compounding issues, both now\nfixed/documented —\n1. **Real source bug, fixed in PR #733**: `qhexrt_session.cpp`'s `runtime_acquire()`\n   called `qhx_runtime_create(nullptr, nullptr)` on every non-Android platform,\n   trusting the OS default DLL search order to find `QnnHtp.dll`/`QnnSystem.dll`.\n   On Windows that order never includes a dynamically-loaded sibling plugin's own\n   directory (`LOAD_WITH_ALTERED_SEARCH_PATH`, used to load `runanywhere_qhexrt.dll`\n   itself, only widens the search for *that* load call, not later `LoadLibrary`\n   calls made by code already running inside it) — so the QNN DLLs were invisible\n   even sitting right next to the plugin. Fixed by resolving the plugin's own\n   module directory (`GetModuleHandleExA`+`GetModuleFileNameA`) and passing\n   explicit paths, mirroring Android's existing `ADSP_LIBRARY_PATH` resolution in\n   the same file.\n2. **Operational staleness, NOT a script bug**: even after (1), the QNN runtime\n   DLLs actually staged into `@runanywhere/electron-qhexrt`'s\n   `prebuilds/win32-arm64` (`node_modules/.../electron-qhexrt/prebuilds/win32-arm64/`)\n   were from an old QAIRT build (~2.45.x vintage, Dec 2025 files) while the box's\n   installed QAIRT was 2.48.0.260626 — a real ABI-relevant version mismatch.\n   `bindings/electron/scripts/bundle-native.ts`'s `stageQnnRuntime()` just mirrors\n   whatever `RA_QNN_RUNTIME_DIR` points at with **no version check** — it's\n   correct-as-written; someone had pointed it at a stale neurun-side\n   `qairt-runtime-248`-named folder that was never refreshed. Fix: rebuild that\n   flat dir from the CURRENT QAIRT SDK's `lib/aarch64-windows-msvc/` (host DLLs:\n   `QnnHtp.dll`, `QnnHtpPrepare.dll`, `QnnHtpV81Stub.dll`,\n   `QnnHtpV81CalculatorStub.dll`, `QnnSystem.dll`) + `lib/hexagon-v81/unsigned/`\n   (DSP side: `libQnnHtpV81Skel.so`, `libqnnhtpv81.cat`), point\n   `RA_QNN_RUNTIME_DIR` at it, and re-run\n   `npm run bundle:native -- --package=qhexrt` from `bindings/electron/`.\n   **The already-published `@runanywhere/electron-qhexrt` npm package (as of\n   0.20.24) almost certainly ships the same stale DLLs** — this needs a\n   human decision (republish/patch) before claiming the published package works\n   on Windows NPU, separate from the source fix in PR #733.\n3. **PD exhaustion** (the pre-existing entry above) remains real and still applies\n   when two sessions genuinely contend for the one physical NPU — but it is not\n   the only explanation, and the log line to actually check first is the native\n   one: subscribe via `window.runanywhere.logging.records()` (after\n   `logging.setDebugMode(true)`) or add a temporary main-process log capture —\n   `app.process().stdout`/`stderr` do NOT carry it, because the addon runs in a\n   separate `utilityProcess.fork()` host, not the Electron main process, and\n   `RAC_LOG` is not an env var the logger reads (it does nothing). The generic\n   `[QHexRT] qhx_runtime_create failed (QNN libs unavailable?)` vs.\n   `qhx_model_load failed: ... N NPU model(s) already resident` log text is what\n   actually distinguishes cause (1)/(2) from cause (3) — always get that line\n   before diagnosing further.\n\n**Workflow gotcha:** if you orchestrate this via the Workflow tool, its agent()\ncalls do NOT reliably block-wait on a long-running remote build the way a direct\npoll loop does — observed once tonight where a workflow's own agents spawned an\ninternal Monitor/poller for the build log and then returned immediately,\nreporting \"still waiting,\" which let the whole workflow finish with nothing\nactually verified. For \"wait N tens-of-minutes for a real native build to\nfinish,\" use the top-level Monitor tool yourself (a shell poll loop over SSH\nchecking for a terminal BUILD_SUCCEEDED/BUILD_FAILED marker line) rather than\ndelegating the wait itself into a workflow agent's own judgment.\n\n### The Windows box's actual layout, and why it's more than a cross-platform check\n\n**This is a Hexagon-NPU-equipped Windows machine** (Snapdragon-class ARM Windows\ndevice, Copilot+ PC), not just a generic x86 Windows box. That makes it the one\nplace a QHexRT/Hexagon backend build+test can produce real device-only truth for\nWindows — matching the parent workspace's own \"x86 is a simulator, device-only\ntruth\" rule (see the top-level `Qualcomm/CLAUDE.md` and `QHexRT/CLAUDE.md`). Don't\ntreat this box as merely a Windows Electron smoke-test target; it's the only real\nHexagon NPU test rig reachable for this platform.\n\nPer the user (2026-08-17), under `~/Downloads/RunAnywhere/` on that box there are\nthree sibling repo checkouts at the same level — **verify each on connect, don't\nassume paths without checking, since this is relayed secondhand and may have\ndrifted**:\n- `runanywhere-sdks` — this monorepo, presumably already cloned; `git fetch && git\n  log -1 origin/main` to see how far behind it is before doing anything.\n- `runanywhere-electron` — the Electron starter app (same repo as\n  `RunanywhereAI/runanywhere-electron` elsewhere in this skill).\n- a third repo referred to verbally as \"the neuron/new-run inference engine for the\n  Hexagon NPU\" — almost certainly **`neurun`**, the QHexRT/NeuRT split-out repo\n  (see memory: \"neurun component split merged — QHexRT/ + NeuRT/ + shared/, root is\n  a closed 20-entry set enforced by a test\"). Confirm the actual directory name and\n  remote (`git remote get-url origin`) on connect rather than guessing from the\n  transcription.\n\n**Full task on this box (per explicit user instruction, not just a smoke test):**\n1. `git pull` (or fetch+fast-forward) all three repos to latest `main`.\n2. Rebuild the QHexRT/Hexagon NPU backend from `neurun` against this device's real\n   Hexagon NPU — this is the one environment where that build+run is meaningful\n   proof, not a simulator claim.\n3. Bump Electron's native dependencies to their latest versions — llama.cpp,\n   sherpa-onnx, ONNX Runtime, and whatever else `bindings/electron` pins — across\n   the repos involved. Treat this as a real dependency-bump task (check for\n   breaking API changes, don't just edit a version string and assume it builds).\n4. Update the model catalog for **all 5 starter apps across all 4 consumer repos**\n   (iOS, Android, Web, Electron — plus whatever the \"5th app\" split means for\n   macOS/Swift, see the rest of this skill) so the catalog is consistent everywhere,\n   not just on this Windows box.\n5. Run a real end-to-end inference pass on this box specifically (not just build\n   success) — both the general Electron/llama.cpp path AND the Hexagon NPU path via\n   `neurun`/QHexRT.\n6. Open PRs for whatever changed, in each affected repo. Do not merge them —\n   the user explicitly said they'll review.\n\nThis is real engineering work (native dependency bumps + an NPU backend rebuild),\nnot a mechanical version-string edit — expect it to take real time and to\ngenuinely need `QHexRT/CLAUDE.md` and `neurun`'s own docs read in full before\ntouching that backend, per the parent workspace's own instructions.\n\n## 8. Reporting\n\nFor each of the 5 apps (iOS, macOS/Swift, Android, Web, Electron), state exactly\none of these two outcomes — never blur them:\n\n- **FULLY VERIFIED** — with concrete evidence: the version string actually\n  resolved/logged, the model used, and the actual output produced (quote it).\n- **BLOCKED** — with the exact concrete reason, e.g.: the Windows ARM64 box's\n  mesh-VPN node is offline; Android emulator unavailable in this environment; App Store /\n  notarization credential missing; `runanywhere-swift` not yet tagged at target\n  version so `swift package update` can't resolve; electron-qhexrt policy conflict\n  needs a human call before proceeding further.\n\nA clearly reported partial/blocked result is far more valuable than an optimistic\nsummary that overstates what was actually observed. If you hit the known Electron\nqhexrt npm-publish policy conflict (§7), call it out in the report even if it\nwasn't the thing blocking your specific test — it's a standing issue every release\nnight should re-surface until a human resolves it.\n\n## Credentials referenced but never printed by this skill\n\nThese exist and are checked for presence only — never print their values:\n\n- Maven Central: `mavenCentral.username` / `mavenCentral.password`,\n  `signing.gnupg.keyName` / `signing.gnupg.passphrase` in `~/.gradle/gradle.properties`.\n- GPG signing key `CC377A9928C7BB18` — check with\n  `gpg --list-secret-keys --keyid-format LONG`.\n- macOS notarization profile `runanywhere-notary` — check with\n  `xcrun notarytool history --keychain-profile runanywhere-notary`; codesign\n  identity `Developer ID Application: RunAnywhere, Inc` — check with\n  `security find-identity -v -p codesigning`.\n- npm — `npm whoami` should already show a logged-in user.\n\nNone of the app-level smoke tests in this skill require touching these directly;\nthey matter only if a test path requires a fresh distro-repo publish (e.g.\n`runanywhere-swift` not yet tagged) — that's a separate concern of the\n`sdk-publish` skill, not this one.\n\n<!-- File: .claude/skills/telemetry-e2e/SKILL.md (Tokens: ~5937 | Category: anthropic-skill) -->\n---\nname: telemetry-e2e\ndescription: Prove that what an SDK binding EMITS on the telemetry wire is correct and that it actually arrives — vocabulary compliance, presence/absence semantics, device-id stability, and a received request body / stored row as evidence. Use when touching telemetry_json.cpp, telemetry_manager.cpp, rac_telemetry_vocabulary.h, any binding's platform-adapter telemetry bridge, or when asked to verify a telemetry field isn't silently dropping.\n---\n\n# Telemetry Wire Correctness (SDK side)\n\nThis is the SDK-side counterpart to the backend's `telemetry-e2e` skill (private\n`Runanywhere-monorepo` repo — proves event-to-stored-row on the server side). This one\nproves the other half: that commons *serializes* a call correctly and that the bytes\n*leaving* a binding are the bytes a real backend accepts. It does not cover per-starter-app\ninference smoke tests — see `sdk-test-starters` for that.\n\n## 0. The evidence bar\n\nCompiling is not evidence. A green `telemetry_extraction_tests` run is not evidence that a\nreal binding's real call site reaches the wire correctly — it only proves the *serializer*\nhandles a hand-built payload correctly. In order of increasing strength:\n\n1. **Unit test green** (`ctest -R telemetry_extraction_tests`) — proves the JSON builder's\n   presence/vocabulary rules are still correct in isolation. Necessary, not sufficient.\n2. **Received request body** — capture the actual JSON `rcli` (or a real binding) put on\n   the wire and read it: right modality endpoint, right keys, no vocabulary values that\n   got through that shouldn't have, measured zeros kept as `0` not `null`.\n3. **Stored row matches, field-for-field** — the backend's own ingest response\n   (`events_received`/`events_stored`/`events_skipped`) or a direct DB read shows the row\n   landed with the values you actually sent, not just a 2xx. This is the only tier that\n   also proves the vocabulary/schema on both sides still agree.\n\nA PR description that says \"sent telemetry, no errors\" without quoting a request body or a\nstored-row count is not evidence of anything except that a socket didn't error out.\n\n## 1. The emit path\n\nEvery binding funnels through the same two C ABI entry points in\n`core/src/infrastructure/telemetry/telemetry_manager.cpp`:\n\n- `rac_telemetry_manager_track` — flat-struct payload (`rac_telemetry_payload_t`), used by\n  `rcli`'s `telemetry emit`/`telemetry blast` and by hand-built events.\n- `rac_telemetry_manager_track_proto` — the real path every language binding actually uses:\n  a binding builds a protobuf `SDKEvent` (`sdk_events.pb.h`) at the real call site (e.g. an\n  LLM `generate()` completing), and `proto_event_type_string()` + the per-domain extraction\n  blocks in `telemetry_manager.cpp` turn it into the same flat payload. `framework_proto_to_string()`\n  / `framework_to_string()` / `clean_framework()` do the backend-name normalization here —\n  this is where a raw enum like `INFERENCE_FRAMEWORK_LLAMA_CPP` becomes the wire string `\"llamacpp\"`.\n- `stamp_live_device_state()` (same file) then overwrites the SDK-origin/device-state fields\n  (`sdk_binding`, `platform`, `battery_state`, memory, core count) from manager-owned state,\n  not from whatever the caller passed — a binding cannot inject a platform or binding value.\n- The payload is handed to `rac_telemetry_manager_payload_to_json()` in\n  `core/src/infrastructure/telemetry/telemetry_json.cpp`, which does field-by-field\n  serialization (see §2/§3 for the two contracts enforced there).\n- `send_batch_json()` batches by modality and POSTs to `/api/v2/sdk/telemetry/{modality}`\n  through whichever `rac_http_transport_ops_t` the binding registered (URLSession/OkHttp/\n  `emscripten_fetch`/curl for `rcli` — see the root `AGENTS.md`'s \"HTTP is platform-provided\"\n  rule). There is no `libcurl` fallback baked into commons itself.\n\nIf a field is going missing on the wire, the fix is almost always in\n`telemetry_json.cpp` (a field never wired into the serializer) or in the call site's proto\npopulation (a field never set on the `SDKEvent`) — not in a binding's bridge code, which\njust forwards bytes.\n\n## 2. The closed vocabularies — and the silent-drop rule\n\n`core/include/rac/infrastructure/telemetry/rac_telemetry_vocabulary.h` is a **generated\nfile** (see §5) holding six closed tables, each with a `RAC_TELEMETRY_<NAME>_COUNT` and a\n`RAC_TELEMETRY_<NAME>_VALUES[]` array:\n\n| Table | Count | Wire field | Values |\n|---|---|---|---|\n| `RAC_TELEMETRY_EVENT_TYPE` | 106 | `event_type` | namespaced strings, e.g. `llm.generation.completed`, `auth.succeeded`, `stt.transcription.completed` |\n| `RAC_TELEMETRY_BACKEND` | 22 | `backend` / `framework` | `onnx, sherpa, llamacpp, foundation_models, system_tts, fluid_audio, coreml, whisperkit_coreml, mlx, qhexrt, neurt, tflite, executorch, mediapipe, mlc, pico_llm, piper_tts, swift_transformers, cloud, builtin, none, unknown` |\n| `RAC_TELEMETRY_MODEL_SOURCE` | 3 | `model_source` | `catalog, builtin, user` |\n| `RAC_TELEMETRY_PLATFORM` | 9 | `platform` | `ios, android, macos, windows, linux, web, tvos, watchos, visionos` |\n| `RAC_TELEMETRY_SDK_BINDING` | 9 | `sdk_binding` | `swift, kotlin, flutter, react-native, web, electron, python, cli, cpp` |\n| `RAC_TELEMETRY_BATTERY_STATE` | 4 | `battery_state` | `charging, full, unplugged, unknown` |\n\n**The rule, verbatim from `add_vocabulary_string()` in `telemetry_json.cpp`:** if a value is\nnon-null but not present in its table, the field is dropped to `null` and the *rest of the\nevent still ships*. This is by design (a bad `backend` string must not cost the whole\nobservation) but it means:\n\n- The backend never sees the bad value, never 422s, never quarantines anything — there is\n  no error path anywhere for this. A field just isn't there.\n- The only client-side signal is a `RAC_LOG_WARNING(\"Telemetry\", \"Dropping %s: '%s' is not\n  in the published vocabulary\", ...)` log line — easy to miss unless you're specifically\n  watching debug logs.\n- `platform`, `sdk_binding`, and `battery_state` are stamped by the manager itself (§1), not\n  supplied by a binding's call site, so in practice only `backend`/`framework` (from the\n  engine/model layer) and `event_type` (only reachable via the proto path, checked at CI\n  time — see §5) are realistic places a new value can silently disappear.\n- `test_telemetry_extraction.cpp`'s vocabulary block (`\"vocab-1\"`/`\"vocab-2\"` cases) is the\n  concrete regression guard: it sends `framework = \"LlamaCpp\"` (a real spelling that used to\n  reach production) and asserts the string never appears on the wire while `model_id` still\n  does. If you touch `add_vocabulary_string()` or add a new vocabulary-gated field, extend\n  that test rather than trusting a manual check.\n\nHistorical motivation (from the header's own comment): before this table existed, stored\nproduction rows had four spellings of `llama.cpp`, three of ONNX, and binding names like\n`\"react-native\"` recorded as a `platform` — this file exists specifically to make that class\nof drift fail in this repo instead of being discovered in a database nobody was watching.\n\n## 3. Presence vs. absence: the \"measured zero\" contract\n\nBefore the fix documented in `telemetry_json.cpp`'s header, extractors read proto3 scalar\nfields directly — which report `0` for a field that was never set — and the old serializer\n(`add_int`/`add_double`) *skipped* zero values to keep payloads small. Result: a genuinely\nmeasured `output_tokens = 0` (an empty completion) and an unmeasured `tokens_per_second`\nboth arrived on the wire as absent, indistinguishable from each other.\n\nThe fix is `has_x`-gated serialization. Two helper families in `telemetry_json.cpp`:\n\n- `add_int_or_null(key, value, is_valid)` / `add_double_or_null(key, value, is_valid)` —\n  emit the real value (including `0`) when `is_valid` is true, else the JSON literal `null`.\n  Non-finite doubles (`NaN`/`Inf`) always degrade to `null` regardless of `is_valid`, because\n  JSON has no such literal and one bad value must not corrupt the whole batch.\n- The `is_valid` argument is a real `has_x`/sentinel check per field, not a blanket flag —\n  e.g. `payload->has_processing_time_ms != RAC_FALSE`, `payload->total_memory > 0`,\n  `payload->battery_level >= 0`.\n\n**What to check to confirm this stayed fixed** (this is exactly what\n`test_telemetry_extraction.cpp`'s presence block does — see §4): send an LLM completion\nevent with `output_tokens = 0` and `input_tokens = 0` explicitly set, and\n`tokens_per_second`/`context_length` left unset. Assert the wire body contains\n`\"output_tokens\":0` and `\"input_tokens\":0` as literal numbers, and `\"tokens_per_second\":null`\n/ `\"context_length\":null` — not all four as `null`, and not all four as `0`. If a future\nrefactor reverts to plain `add_int`/`add_double` for any of these fields, that regression\ntest is the thing that catches it; grep `telemetry_json.cpp` for `add_int(` / `add_double(`\n(the non-`_or_null`/non-`_always` forms) if you suspect a field regressed — those two still\nsilently skip zero by design and must never be used for a field where `0` is meaningful.\n\n## 4. Build, test, and what each telemetry test asserts (macOS)\n\n```bash\ncmake --preset macos-debug\ncmake --build build/macos-debug\nctest --preset macos-debug --output-on-failure -R 'telemetry|device_identity'\n```\n\nOr the non-preset form (`core/AGENTS.md`'s documented path):\n\n```bash\ncmake -B build -DRAC_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Debug\ncmake --build build\nctest --test-dir build --output-on-failure\n```\n\n`telemetry_extraction_tests` and `device_identity_tests` are registered in\n`core/tests/CMakeLists.txt`; `rcli_telemetry_live_tests` is registered separately in\n`rcli/tests/CMakeLists.txt`. All three are hand-rolled assertion runners — `CHECK()` in\nsome files, `ASSERT_EQ`/`ASSERT_TRUE` (from `core/tests/test_common.h`) in others — never\nGoogleTest:\n\n| ctest name | Binary | What it asserts |\n|---|---|---|\n| `telemetry_extraction_tests` | `test_telemetry_extraction` | Full proto→JSON pipeline per modality (LLM token counts/`tokens_per_second`, STT NaN-confidence never leaks as the string `\"nan\"`, TTS `characters_per_second`, embeddings `total_tokens`/`batch_size`/`embedding_dimension`, LoRA failure-path fields, RAG retrieval counts, VLM image/timing fields, live device-state stamping); the presence/measured-zero contract (§3); the closed-vocabulary drop rule (§2); and an HTTP-retry backoff (a failed batch is re-sent only after >5s, never before). Requires `RAC_HAVE_PROTOBUF` — the test binary itself no-ops with exit 0 if commons wasn't built with protobuf support. |\n| `device_identity_tests` | `test_device_identity` | Every branch of `rac_device_get_or_create_persistent_id()`'s resolution chain (§6) against a mocked platform adapter — cache hit, vendor-ID fallback, fresh-UUID generation, and that a `secure_set` failure doesn't silently drop the ID. |\n| `rcli_telemetry_live_tests` | `test_rcli_telemetry_live` | **Opt-in, always registered but a no-op by default.** Runs only with `--live` *and* `RUNANYWHERE_BASE_URL`/`RUNANYWHERE_API_KEY` set; otherwise prints \"skip\" and exits 0 — a green ctest run for this target proves nothing by itself. When live, it runs `rcli::bootstrap()` (real API-key → device-register → JWT handshake), sends one real event per modality (LLM, embeddings, RAG, VLM, LoRA-failure) through the real HTTP transport, and fails on any non-2xx response — this is the test that would catch a strict-schema `422 extra_forbidden` from field drift against the real V2 endpoints. |\n\nRun it live (§7 has the full local-backend flow):\n\n```bash\nRUNANYWHERE_BASE_URL=https://<backend-origin> RUNANYWHERE_API_KEY=<key> \\\n  ./build/macos-debug/rcli/tests/test_rcli_telemetry_live --live\n```\n\n`test_platform_bridge` (same `CMakeLists.txt`, adjacent target) is the wider companion\ncheck — \"every value a binding supplies must reach the wire\" — covering the platform half\nof the same two flat C structs (`rac_client_info_t`/`rac_device_registration_info_t`) for\nevery SDK at once; run it alongside if you're touching client-info fields rather than\ntelemetry event fields specifically.\n\n## 5. Regenerating the vocabulary header, and the drift gate\n\n`rac_telemetry_vocabulary.h` (§2) is generated from `idl/http/sdk-openapi.json` — the\ndevice-facing OpenAPI subset copied in from the private backend repo. **How that file gets\nupdated on the backend side, and how it crosses into this repo, is the monorepo's\n`openapi-contract` skill's job — read that by name in the other repo rather than\nre-deriving the steps here.** From inside this repo, once `idl/http/sdk-openapi.json` is\ncurrent:\n\n```bash\npython3 idl/codegen/generate_telemetry_vocabulary.py          # regenerate the header\npython3 idl/codegen/generate_telemetry_vocabulary.py --check  # CI drift check; exit 1 if stale\n```\n\n`.github/workflows/telemetry-vocabulary-drift.yml` runs three checks on every push to\n`main`/`development` and on any PR touching `idl/http/**`, the codegen script, or the\ntelemetry source/header dirs:\n\n1. **Generated header is current** — the `--check` command above.\n2. **Every event type the core can emit is in the vocabulary** — a Python check that parses\n   `proto_event_type_string()`'s body out of `telemetry_manager.cpp` via regex, unions in\n   the two prefix-expansion families (`stt.model.*`/`tts.voice.*`/`llm.model.*` and\n   `llm*`/`vlm*` generation suffixes), and diffs against `TelemetryEventType`'s enum in\n   `idl/http/sdk-openapi.json`. Anything emittable but unpublished fails the job.\n3. **Every framework string the core can emit is in the vocabulary** — same technique\n   against `framework_proto_to_string()` and `framework_to_string()` vs. `TelemetryBackend`.\n\n**A documented drift you will hit if you trust the wrong file**: `idl/http/README.md`'s own\n\"What it pins\" table names the schema for the `framework`/`backend` field as\n`TelemetryFramework` — that's stale. The actual generator\n(`idl/codegen/generate_telemetry_vocabulary.py`'s `VOCABULARIES` dict) and the header it\nproduces both say `TelemetryBackend`. Trust the generator and the generated header's own\n`// Published as X` comments over that README table.\n\n## 6. Per-binding device identity: where it's created, where it's persisted\n\nThe resolution chain itself is centralized in commons —\n`core/src/infrastructure/device/device_identity.cpp`\n(`rac_device_get_or_create_persistent_id`, tested by `device_identity_tests`, §4) — and is\nthe same fixed order for every binding:\n\n1. `adapter.secure_get(\"com.runanywhere.sdk.device.uuid\")` — if found and non-empty, use it,\n   full stop.\n2. `adapter.get_vendor_id` (if the platform adapter provides one) — use it, then persist it\n   via `secure_set` so step 1 hits next boot.\n3. Generate a fresh RFC-4122 v4 UUID, persist it via `secure_set`.\n\nWhat differs per binding is **only** the `secure_get`/`secure_set` backing store — an\nunstable backing store (one that doesn't survive a real app restart, or that resolves to a\ndifferent physical location than the read path expects) is exactly the bug class that\ninflates device counts, because step 3 fires again on every \"cache miss\":\n\n| Binding | Backing store | Where |\n|---|---|---|\n| Swift (iOS/macOS) | Keychain, service `com.runanywhere.sdk` | `bindings/swift/Sources/RunAnywhere/Foundation/Bridge/Extensions/CppBridge+PlatformAdapter.swift` (`platformSecureGetCallback`/`platformSecureSetCallback`) |\n| Kotlin (Android) | Android Keystore AES-256-GCM key, ciphertext in a no-backup app file (not `SharedPreferences`) | `bindings/kotlin/src/main/kotlin/com/runanywhere/sdk/foundation/security/AndroidKeychainManager.kt` (`NoBackupCiphertextStore`), wired through `CppBridgePlatformAdapter.PlatformSecureStorage` |\n| Flutter | Native Keychain/Keystore helper via FFI symbols `ra_flutter_secure_storage_{store,retrieve,delete}` | `bindings/flutter/packages/runanywhere/lib/native/dart_bridge_secure_storage.dart`; the device-UUID key constant is duplicated (deliberately, per its own comment) as `_keyDeviceUUID = 'com.runanywhere.sdk.device.uuid'` in `dart_bridge_device.dart` |\n| React Native (Android) | `SecureStorageManager` (native Android side) via `PlatformAdapterBridge.kt`'s `secureGet`/`secureSet` | `bindings/react-native/packages/core/android/src/main/java/com/margelo/nitro/runanywhere/PlatformAdapterBridge.kt` |\n| Web | `localStorage`, key `rac_sdk_plaintext_com.runanywhere.sdk.device.uuid` — **plaintext**, no OS-level encryption equivalent exists in a browser; restricted by convention to non-sensitive IDs | `bindings/web/packages/core/src/runtime/PlatformAdapter.ts` (`registerSecureGet`/`registerSecureSet`); also has a separate `stableVendorId()` fallback under key `rac_sdk_plaintext_vendor_id` |\n| Electron (macOS/Linux) | Flat file under a secure directory, path-traversal-guarded (`fs::path(key)` must not be absolute or contain `..`) | `bindings/electron/native/posix_platform_adapter.cpp` (`posix_secure_get`/`posix_secure_set`) |\n| Electron (Windows) | Windows DPAPI (`CryptProtectData`, current-user scope, no UI) | `bindings/electron/native/win32_platform_adapter.cpp` (`win_secure_get`/`win_secure_set`) |\n\nThe key string itself — `com.runanywhere.sdk.device.uuid` — is intentionally the same\nliteral across Swift, Flutter, and RN (each source comments cross-reference the other two by\nname), so grepping for it is a fast way to confirm a new binding is reading/writing the\n*same* key it thinks it is, not a typo'd sibling.\n\n**How to verify stability across a real restart** (not just \"the code looks right\"):\n\n1. Run the binding once, capture the emitted `device_id` from a real request body (§7) or\n   from `POST /api/v1/devices/register`'s response.\n2. Fully terminate the process (not just background it — on mobile this matters, a\n   backgrounded app can keep its in-memory device-id cache alive without ever touching\n   the read path again).\n3. Relaunch and repeat step 1.\n4. The `device_id` must be byte-identical. If it changed, the break is almost always one of:\n   the backing store not surviving process death (e.g. an in-memory-only stub used during\n   development), a key-name mismatch between the write and read paths, or (Android\n   specifically) `Context.deleteSharedPreferences` being invoked on the wrong file — see\n   `AndroidKeychainManager.kt`'s constructor, which deletes a hardcoded *legacy* prefs name\n   on every construction; if that name were ever widened to match the real store, every\n   Android launch would look like a fresh device.\n\nTwo-in-one launches on the *same physical device* (e.g. two emulator instances, or a\nsame-device reinstall vs. a real fresh install) are a different, real-world failure mode\nfrom a code bug — don't conflate an intentional reinstall's fresh ID with instability.\n\n## 7. Point a local build at a local backend and prove an event landed\n\n`rcli` is the fastest way to exercise the full wire path without a mobile/browser\ntoolchain — it links `rac_commons` directly and speaks real HTTP via curl.\n\n```bash\ncmake --preset rcli-macos-release   # or rcli-linux-release on Linux\ncmake --build build/rcli-macos-release --target rcli -j \"$(sysctl -n hw.logicalcpu)\"\n```\n\n**Keyless (development environment) — no API key, works against any backend that allows\nanonymous V2 telemetry:**\n\n```bash\n./build/rcli-macos-release/rcli/rcli --environment development \\\n  --base-url http://127.0.0.1:8000 \\\n  telemetry blast --processing-ms 42.5 --session-id my-check \\\n  --input-tokens 128 --output-tokens 256\n```\n\nThis is exactly the shape `scripts/ci/oss_keyless_telemetry_blast.sh` runs in CI against the\npublic staging backend — `--environment development --base-url <origin> telemetry blast\n...` with no `--api-key`. `telemetry blast` sends one representative event per modality (all\n12) and prints a `MODALITY | RESULT | STATUS | RECEIVED | STORED | SKIPPED` table parsed\ndirectly from the backend's own batch response — **the `STORED` column is authoritative\nserver-side confirmation**, not just an HTTP 2xx. `run_telemetry_blast()`'s own exit-code\nlogic (`rcli/src/commands/cmd_telemetry.cpp`) does fold `stored >= count` into its overall\npass/fail, so a genuine storage shortfall on any modality does make the process exit\nnon-zero, not just a transport error — but the exit code is one bit for all 12 modalities\ncombined, so a non-zero exit still requires reading the table to know *which* modality\nfailed and *why* (a `network error` status is a transport problem; an `HTTP 200` row with\n`STORED` under `RECEIVED` is a row-level rejection, e.g. a vocabulary value the backend\ndidn't recognize — a real bug to chase, not the same failure class at all).\n\n**Authenticated (production shape) — for exercising the login handshake too:**\n\n```bash\n./build/rcli-macos-release/rcli/rcli --environment production \\\n  --base-url https://<backend-origin> --api-key <key> \\\n  auth login\n./build/rcli-macos-release/rcli/rcli --environment production \\\n  --base-url https://<backend-origin> --api-key <key> \\\n  telemetry emit --modality llm --count 3 --input-tokens 128 --output-tokens 256\n```\n\n`auth login` only has a path in `production` — `development`'s keyless mode has no login\nstep by design (see `rcli/src/commands/cmd_auth.cpp`'s own subcommand help text).\n\n**To inspect the actual request body**, not just the summary table, add `--json` for\nmachine-readable CLI output and `-v` for debug logging (the debug log includes the\nserialized JSON per batch before it goes on the wire) — or point `--base-url` at a throwaway\nlocal capture endpoint (any script that logs the raw POST body) before pointing it at a real\nbackend, to sanity-check field shape offline first.\n\n**If you have the private backend repo checked out locally too**, running it yourself\n(`cd backend && uv run uvicorn main:app --reload`, per that repo's own README) and pointing\n`--base-url` at `http://127.0.0.1:8000` gets you the strongest possible evidence: query the\nSQLite/Postgres row directly after a blast and diff it field-for-field against what you sent\n— the monorepo's own `telemetry-e2e` skill (its integration suite, hermetic pytest fixtures,\nand `test_telemetry_conformance.py`'s vocabulary/presence/identity assertions against stored\nrows) is the authoritative reference for that side; don't re-implement its DB assertions\nhere, just point at it.\n\n## Evidence bar, restated\n\n- Compiling `rcli` or a binding: **not evidence**.\n- `ctest -R telemetry_extraction_tests` green: **not evidence of a real call site** — only\n  of the serializer in isolation.\n- `rcli telemetry blast` exiting 0: real signal (its exit code does fold in the backend's\n  `stored >= count` check, §7) but still worth pairing with the per-modality\n  `STORED`/`SKIPPED` table in your report — the exit code can't tell you *which* modality\n  or *why* a shortfall happened.\n- A captured request body showing the right endpoint, the right keys, correct\n  presence/absence per §3, and no off-vocabulary values per §2 — **evidence of correct\n  emission**.\n- That request body's values matching a stored row (via the backend's own\n  `events_stored` count at minimum, a direct DB read at best) — **evidence of correct\n  end-to-end delivery**, the only tier worth reporting as \"telemetry works.\"\n\n<!-- File: bindings/electron/packages/llamacpp/package-lock.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Llamacpp Skill\n\nPath: `bindings/electron/packages/llamacpp/package-lock.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/llamacpp/package-lock.json)\n\n<!-- File: bindings/electron/packages/llamacpp/package.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Llamacpp Skill\n\nPath: `bindings/electron/packages/llamacpp/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/llamacpp/package.json)\n\n<!-- File: bindings/electron/packages/llamacpp/tsconfig.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Llamacpp Skill\n\nPath: `bindings/electron/packages/llamacpp/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/llamacpp/tsconfig.json)\n\n<!-- File: bindings/electron/packages/neurt/package-lock.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Neurt Skill\n\nPath: `bindings/electron/packages/neurt/package-lock.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/neurt/package-lock.json)\n\n<!-- File: bindings/electron/packages/neurt/package.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Neurt Skill\n\nPath: `bindings/electron/packages/neurt/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/neurt/package.json)\n\n<!-- File: bindings/electron/packages/neurt/tsconfig.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Neurt Skill\n\nPath: `bindings/electron/packages/neurt/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/neurt/tsconfig.json)\n\n<!-- File: bindings/electron/packages/onnx/package-lock.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Onnx Skill\n\nPath: `bindings/electron/packages/onnx/package-lock.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/onnx/package-lock.json)\n\n<!-- File: bindings/electron/packages/onnx/package.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Onnx Skill\n\nPath: `bindings/electron/packages/onnx/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/onnx/package.json)\n\n<!-- File: bindings/electron/packages/onnx/tsconfig.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Onnx Skill\n\nPath: `bindings/electron/packages/onnx/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/onnx/tsconfig.json)\n\n<!-- File: bindings/electron/packages/qhexrt/package-lock.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Qhexrt Skill\n\nPath: `bindings/electron/packages/qhexrt/package-lock.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/qhexrt/package-lock.json)\n\n<!-- File: bindings/electron/packages/qhexrt/package.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Qhexrt Skill\n\nPath: `bindings/electron/packages/qhexrt/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/qhexrt/package.json)\n\n<!-- File: bindings/electron/packages/qhexrt/tsconfig.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Qhexrt Skill\n\nPath: `bindings/electron/packages/qhexrt/tsconfig.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/qhexrt/tsconfig.json)\n\n<!-- File: bindings/electron/packages/sherpa/package-lock.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Sherpa Skill\n\nPath: `bindings/electron/packages/sherpa/package-lock.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/sherpa/package-lock.json)\n\n<!-- File: bindings/electron/packages/sherpa/package.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Sherpa Skill\n\nPath: `bindings/electron/packages/sherpa/package.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/RunanywhereAI/runanywhere-sdks/HEAD/bindings/electron/packages/sherpa/package.json)\n\n<!-- ... +61 more modular skills available via /api/skills/RunanywhereAI/runanywhere-sdks?path={path} -->\n</yakaai_skills>"}