From 4e7f15c8759c7ca6f119d85f3c64570e9d5f26e1 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Tue, 20 Oct 2026 16:41:04 -0700 Subject: [PATCH 1/3] fix: [SDK-5583] open the served web page with the OS opener when no browser tool exists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vite and most dev servers do not open a tab, and without a browser tool the verify skill told the user to open the URL themselves and then asked for the subscription ID. Part A → Web now runs the OS opener (`open`, `xdg-open`, `start "" `) on the served URL before any ask, once per run, and falls back to the manual instruction only when the opener is missing, fails, or the host blocks it. The step 1 run bullet, the step 2 no-key ask, the poll-loop reminder, and the inputs table say the same. --- skills/verify/SKILL.md | 8 ++++---- skills/verify/platform-verification.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/skills/verify/SKILL.md b/skills/verify/SKILL.md index 5e470d5..2a3467f 100644 --- a/skills/verify/SKILL.md +++ b/skills/verify/SKILL.md @@ -78,7 +78,7 @@ Do not ask twice. A refusal is a valid answer: checkpoints stay local, and you n | App ID | Public; from the init code you can grep, or the prior skill's summary | every API step | | App-scoped key | provided by the setup flow / invocation, or env `$ONESIGNAL_REST_API_KEY` / `$ONESIGNAL_SETUP_TOKEN`; if none of these, MCP or the Keys & IDs link below | steps 2, 4–5 (unless MCP) | | MCP connected? | Check for the OneSignal MCP tools (`onesignal_health`, `send_message`, `view_user`, `view_message`); if the tools are absent, check for a registered-but-unauthenticated server before you fall back (see "No key → MCP first" below) | preferred path for 4–5 | -| Browser tool? (web only) | Check the session's tool list for a browser automation tool — for example the Claude in Chrome extension, a Playwright or Chrome DevTools MCP server, or the Cursor browser. Use only a tool the session exposes; never invent the call (safety contract §14a) | step 1 page load, step 2 page-side subscription read (no key needed) | +| Browser tool? (web only) | Check the session's tool list for a browser automation tool — for example the Claude in Chrome extension, a Playwright or Chrome DevTools MCP server, or the Cursor browser. Use only a tool the session exposes; never invent the call (safety contract §14a). Without one, the OS opener (`open` / `xdg-open` / `start`) still puts the page in front of the user; it cannot read the page | step 1 page load, step 2 page-side subscription read (no key needed) | | external_id (only if the app wires `login`) | Grep the wrapper for the `login(...)` call | step 3 identity check | **No key → MCP first, Keys & IDs link second.** Resolve in this order — never jump straight to the key ask: @@ -118,7 +118,7 @@ The device cannot register until the app actually runs with the SDK linked. Pick - Run builds via the user's own package manager (detect via lockfile — safety contract §7). Do not add or bump dependencies. - If the build fails, report the compiler error verbatim and STOP. A build break is not a OneSignal problem yet; hand it back or route to the setup skill if the failure is in OneSignal wiring. Do not proceed to API steps against an app that never ran. -- **Run the app yourself when a simulator or a local server can serve.** On an Apple-silicon Mac, boot, install, and launch the iOS simulator yourself — unattended, but not headless: keep the Simulator window open for the user's later permission tap (exact `simctl` sequence in platform-verification.md Part A). On web, when the session exposes a browser tool, open the served page in it yourself (platform-verification.md Part A → Web). Do not ask the user to do a run you can do. A physical phone is different: you cannot drive it, so hand that launch to the user and wait. +- **Run the app yourself when a simulator or a local server can serve.** On an Apple-silicon Mac, boot, install, and launch the iOS simulator yourself — unattended, but not headless: keep the Simulator window open for the user's later permission tap (exact `simctl` sequence in platform-verification.md Part A). On web, when the session exposes a browser tool, open the served page in it yourself; without one, open the served URL in the user's default browser with the OS opener (`open` / `xdg-open` / `start`) before you ask them to do anything — dev servers such as Vite do not open a tab on their own (platform-verification.md Part A → Web). Do not ask the user to do a run you can do. A physical phone is different: you cannot drive it, so hand that launch to the user and wait. - **You cannot press "Allow" anywhere — simulator included** (`xcrun simctl privacy` has no notifications service). On iOS the prompt does not gate step 2: with the setup skill's `UIBackgroundModes = remote-notification` edit, the SDK creates the server-side subscription before the prompt is answered (SDK-verified; detail in platform-verification.md Part A). The accepted prompt is needed from step 4 on — tell the user the single tap you need from them, and when. If the setup skill did not write this integration, confirm `remote-notification` is in `Info.plist` first: without that background mode, the prompt gates step 2 too, and a step-2 timeout means "tap not done", not "credentials missing". ### Step 2 — Subscription presence (poll until first device registers) @@ -126,9 +126,9 @@ The device cannot register until the app actually runs with the SDK linked. Pick This is the dashboard signup wizard's own pattern: fetch subscriptions/players with `limit: 1` — a non-empty result means the first subscriber exists (see api-reference.md "Subscriber presence poll"). Deterministic equivalents for the REST probes in this skill: `python3 /scripts/onesignal_api.py subscribers|notification-stats|web-probe` (prefer the MCP if connected). The `notification-stats` output labels `failed` as unsubscribed targets, not delivery errors. - **Web — read the page (no key needed; preferred on web):** when the session exposes a browser tool (see "Inputs you need"), open the served page in it and evaluate `OneSignal.User.PushSubscription.id` and `OneSignal.User.PushSubscription.optedIn` in the page. The debug helper also prints `[OneSignal] Push subscription registered: ` to the console. A non-empty `id` is server-assigned — the SDK returns `undefined` while the ID is still local, and on web the SDK creates the subscription only after the browser granted permission and registered (verified in SDK source: `PushSubscriptionNamespace.id`, `updatePushSubscriptionModelWithRawSubscription`). `optedIn === true` is the page-side equivalent of `notification_types >= 1`; read both. Poll on the same cadence as the REST loop below. The browser's native permission dialog is not part of the page: the user clicks Allow on the slidedown and then in the dialog — say that once, then read the values. **Read `Notification.permission` from the page with each poll — it separates "no click yet" from a real failure**, the same way `notification_types < 1` does on iOS: `"default"` with no `id` means the user did not click Allow yet — report "waiting on the Allow click", ask for the click, and keep polling past the timeout; `"denied"` means the user blocked the prompt → step 7 §5, never §1; `"granted"` with no `id` after the timeout is the real step-2 timeout → step 7 as written. Never send the user to the dashboard or the console to look up the subscription ID while a tool in the session can read it. -- **MCP path:** there is no suitable MCP tool for a presence poll — `export_subscriptions_csv` is a whole-audience async export (capped at 1 concurrent run per account), and `estimate_recipient_count` is email-only and returns 0 for push. Use the page read above (web) or the REST poll below (a key is still required). If only the MCP is available, no key, and no browser tool: tell the user this rung needs a key, and offer one alternative through the structured-question tool — the ID from the debug helper's console line `[OneSignal] Push subscription registered: `. Name that exact line and where the console is; do not send them to the dashboard for it. +- **MCP path:** there is no suitable MCP tool for a presence poll — `export_subscriptions_csv` is a whole-audience async export (capped at 1 concurrent run per account), and `estimate_recipient_count` is email-only and returns 0 for push. Use the page read above (web) or the REST poll below (a key is still required). If only the MCP is available, no key, and no browser tool: tell the user this rung needs a key, and offer one alternative through the structured-question tool — the ID from the debug helper's console line `[OneSignal] Push subscription registered: `. The page is already open in their browser from step 1 (the OS opener), so the ask starts at the Allow click, not at "open this URL"; name that exact line and where the console is; do not send them to the dashboard for it. - **REST path:** `GET https://api.onesignal.com/players?app_id=&limit=1` with `Authorization: Key `. **Note: `/players` is the legacy Devices API — documented but marked deprecated** ("View players" reference page). Its documented auth form is `Authorization: Basic `; try `Key` first with the current key and fall back to `Basic` if rejected. If the endpoint errors entirely, fall back to the dashboard (Audience → Subscriptions) and have the user confirm the row appeared. The only thing you assert from this call is *non-empty ⇒ a subscriber registered*. -- **Poll loop:** every ~5s, up to a **2-minute timeout**. Between polls: if you launched the app yourself (iOS simulator), tail the launch's `console.log` (the run step prints its path — platform-verification.md Part A) for the debug helper's registration line instead of nagging; if you opened the web page yourself, re-read the page values; otherwise remind the user to launch the app on a real device/browser and accept the notification permission prompt. +- **Poll loop:** every ~5s, up to a **2-minute timeout**. Between polls: if you launched the app yourself (iOS simulator), tail the launch's `console.log` (the run step prints its path — platform-verification.md Part A) for the debug helper's registration line instead of nagging; if you opened the web page in a browser tool, re-read the page values; if you opened it with the OS opener, the tab is already in front of the user, so remind them of the Allow click only, and do not reopen the page; otherwise remind the user to launch the app on a real device/browser and accept the notification permission prompt. - **Read `notification_types` on the returned player — presence alone is not opt-in.** `notification_types >= 1` means registered AND opted in — the activation ladder's bar (platform-matrix.md). A row with `notification_types < 1` on iOS is the normal pre-tap state (the device registers before the prompt is answered — see step 1): report "registered — waiting on the permission tap", ask the user for the tap, and keep polling until the value reaches `>= 1`. **Do not run step 4 against a target below 1** — that send errors as unsubscribed, and step 7 §1 would misread it as a credentials gap. The script reports this as `first_subscription_opted_in`. - **A real subscription ID is server-assigned and is NOT prefixed `local-`.** The SDK assigns a `local-` placeholder before the device registers; a `local-` id does not count as registered (verified against the SDK-ai-prompts verification-flow contract). - **On timeout (still empty):** STOP polling and go to the troubleshooting tree (step 7). On Android/web the overwhelmingly common cause is *missing platform credentials* → route to the credentials skill; on iOS a timeout more likely means the app never ran or init never fired (step 7 §1 pre-check). Do not fabricate a subscription. diff --git a/skills/verify/platform-verification.md b/skills/verify/platform-verification.md index 59fc1fd..9da4c71 100644 --- a/skills/verify/platform-verification.md +++ b/skills/verify/platform-verification.md @@ -18,7 +18,7 @@ The goal of the gate is only to get the app *running with the SDK linked* so the - **Platform-provisioned probe (run FIRST — free, no auth):** `curl -s "https://api.onesignal.com/sync//web?fresh=$(date +%s)"`. `success: true` ⇒ the web platform is configured. `{"success":false,"code":2,"description":"This app is not configured for web push."}` ⇒ the dashboard web-platform step never happened — go straight to Part B §2 first bullet; nothing else can pass until it's fixed. Always include the throwaway query param: responses are CDN-cached ~1 h (`Cache-Control: public, max-age=3600`, `Vary: Origin`), so the plain URL can serve a stale answer. - **Pass condition:** sync probe returns `success: true`; page loads; the init script tag is present; **`OneSignalSDKWorker.js` is reachable same-origin** — open `https:///OneSignalSDKWorker.js` (or the configured path) and confirm it returns the one-line `importScripts(...)` body with `Content-Type: application/javascript`, no redirect. A 404/403/redirect/wrong-MIME here is the most common web failure once the platform is provisioned (Part B §2). - **Open the page yourself when the session exposes a browser tool** (SKILL.md "Inputs you need"): load the served URL in it, then read `OneSignal.User.PushSubscription.id` and `.optedIn` from the page for step 2 — no REST key needed. When the setup skill wrote the integration, its debug helper opens the OneSignal slidedown about 2 seconds after init; the user clicks Allow on the slidedown and then in the browser's native dialog. Tell them that once, then read the values. The native dialog is outside the page, so no tool clicks it. -- Without a browser tool, instruct the user to open a fresh tab (not just refresh; a new tab triggers full SDK init), click Allow on the slidedown and in the native dialog, then poll step 2. +- **Without a browser tool, open the page in the user's default browser yourself** before you ask them for anything: Vite and most dev servers do not open a tab on their own, and a user who sees only a URL in the chat has to notice it and open it. Run the OS opener on the served URL — macOS `open `, Linux `xdg-open `, Windows `start "" ` (`Start-Process ` in PowerShell). Each opens a new tab, which is what the SDK needs (a new tab runs the full init; a refresh does not always). Then tell the user once: the tab is open, click Allow on the slidedown and in the browser's native dialog. If the opener is missing, exits non-zero, or the host blocks the command (a sandboxed runtime can), fall back to the instruction: open a fresh tab at ``, click Allow on the slidedown and in the native dialog. Either way, poll step 2 next. Open the page once per run; do not reopen it on every poll. ### Android - **Build:** `./gradlew assembleDebug` (or `:app:assembleDebug`) from the project root. If the wrapper is missing, use the user's documented build command. From c4e91269cbee3ca080a72304bec0a768a1083a36 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Tue, 20 Oct 2026 16:59:28 -0700 Subject: [PATCH 2/3] fix: [SDK-5583] one Windows opener form, Chromium note for localhost, conditional step-2 ask - Windows uses `cmd /c start "" `, which works from cmd.exe, PowerShell, and Git Bash; the bare `start ""` form fails in PowerShell. - When the URL is a localhost origin, the agent tells the user the page must be in Chrome or Edge, because only Chromium treats localhost as a secure origin for push; on macOS it can target Chrome with `open -a`. - The opener runs after the pass condition and only on an http(s) URL the dev server printed. - The step 2 no-key ask includes the URL when the opener fell back. --- skills/verify/SKILL.md | 6 +++--- skills/verify/platform-verification.md | 7 ++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/skills/verify/SKILL.md b/skills/verify/SKILL.md index 2a3467f..5814468 100644 --- a/skills/verify/SKILL.md +++ b/skills/verify/SKILL.md @@ -78,7 +78,7 @@ Do not ask twice. A refusal is a valid answer: checkpoints stay local, and you n | App ID | Public; from the init code you can grep, or the prior skill's summary | every API step | | App-scoped key | provided by the setup flow / invocation, or env `$ONESIGNAL_REST_API_KEY` / `$ONESIGNAL_SETUP_TOKEN`; if none of these, MCP or the Keys & IDs link below | steps 2, 4–5 (unless MCP) | | MCP connected? | Check for the OneSignal MCP tools (`onesignal_health`, `send_message`, `view_user`, `view_message`); if the tools are absent, check for a registered-but-unauthenticated server before you fall back (see "No key → MCP first" below) | preferred path for 4–5 | -| Browser tool? (web only) | Check the session's tool list for a browser automation tool — for example the Claude in Chrome extension, a Playwright or Chrome DevTools MCP server, or the Cursor browser. Use only a tool the session exposes; never invent the call (safety contract §14a). Without one, the OS opener (`open` / `xdg-open` / `start`) still puts the page in front of the user; it cannot read the page | step 1 page load, step 2 page-side subscription read (no key needed) | +| Browser tool? (web only) | Check the session's tool list for a browser automation tool — for example the Claude in Chrome extension, a Playwright or Chrome DevTools MCP server, or the Cursor browser. Use only a tool the session exposes; never invent the call (safety contract §14a). Without one, the OS opener (`open` / `xdg-open` / `cmd /c start`) still puts the page in front of the user; it cannot read the page | step 1 page load, step 2 page-side subscription read (no key needed) | | external_id (only if the app wires `login`) | Grep the wrapper for the `login(...)` call | step 3 identity check | **No key → MCP first, Keys & IDs link second.** Resolve in this order — never jump straight to the key ask: @@ -118,7 +118,7 @@ The device cannot register until the app actually runs with the SDK linked. Pick - Run builds via the user's own package manager (detect via lockfile — safety contract §7). Do not add or bump dependencies. - If the build fails, report the compiler error verbatim and STOP. A build break is not a OneSignal problem yet; hand it back or route to the setup skill if the failure is in OneSignal wiring. Do not proceed to API steps against an app that never ran. -- **Run the app yourself when a simulator or a local server can serve.** On an Apple-silicon Mac, boot, install, and launch the iOS simulator yourself — unattended, but not headless: keep the Simulator window open for the user's later permission tap (exact `simctl` sequence in platform-verification.md Part A). On web, when the session exposes a browser tool, open the served page in it yourself; without one, open the served URL in the user's default browser with the OS opener (`open` / `xdg-open` / `start`) before you ask them to do anything — dev servers such as Vite do not open a tab on their own (platform-verification.md Part A → Web). Do not ask the user to do a run you can do. A physical phone is different: you cannot drive it, so hand that launch to the user and wait. +- **Run the app yourself when a simulator or a local server can serve.** On an Apple-silicon Mac, boot, install, and launch the iOS simulator yourself — unattended, but not headless: keep the Simulator window open for the user's later permission tap (exact `simctl` sequence in platform-verification.md Part A). On web, when the session exposes a browser tool, open the served page in it yourself; without one, open the served URL in the user's default browser with the OS opener (`open` / `xdg-open` / `cmd /c start`) before you ask them to do anything — dev servers such as Vite do not open a tab on their own (platform-verification.md Part A → Web). Do not ask the user to do a run you can do. A physical phone is different: you cannot drive it, so hand that launch to the user and wait. - **You cannot press "Allow" anywhere — simulator included** (`xcrun simctl privacy` has no notifications service). On iOS the prompt does not gate step 2: with the setup skill's `UIBackgroundModes = remote-notification` edit, the SDK creates the server-side subscription before the prompt is answered (SDK-verified; detail in platform-verification.md Part A). The accepted prompt is needed from step 4 on — tell the user the single tap you need from them, and when. If the setup skill did not write this integration, confirm `remote-notification` is in `Info.plist` first: without that background mode, the prompt gates step 2 too, and a step-2 timeout means "tap not done", not "credentials missing". ### Step 2 — Subscription presence (poll until first device registers) @@ -126,7 +126,7 @@ The device cannot register until the app actually runs with the SDK linked. Pick This is the dashboard signup wizard's own pattern: fetch subscriptions/players with `limit: 1` — a non-empty result means the first subscriber exists (see api-reference.md "Subscriber presence poll"). Deterministic equivalents for the REST probes in this skill: `python3 /scripts/onesignal_api.py subscribers|notification-stats|web-probe` (prefer the MCP if connected). The `notification-stats` output labels `failed` as unsubscribed targets, not delivery errors. - **Web — read the page (no key needed; preferred on web):** when the session exposes a browser tool (see "Inputs you need"), open the served page in it and evaluate `OneSignal.User.PushSubscription.id` and `OneSignal.User.PushSubscription.optedIn` in the page. The debug helper also prints `[OneSignal] Push subscription registered: ` to the console. A non-empty `id` is server-assigned — the SDK returns `undefined` while the ID is still local, and on web the SDK creates the subscription only after the browser granted permission and registered (verified in SDK source: `PushSubscriptionNamespace.id`, `updatePushSubscriptionModelWithRawSubscription`). `optedIn === true` is the page-side equivalent of `notification_types >= 1`; read both. Poll on the same cadence as the REST loop below. The browser's native permission dialog is not part of the page: the user clicks Allow on the slidedown and then in the dialog — say that once, then read the values. **Read `Notification.permission` from the page with each poll — it separates "no click yet" from a real failure**, the same way `notification_types < 1` does on iOS: `"default"` with no `id` means the user did not click Allow yet — report "waiting on the Allow click", ask for the click, and keep polling past the timeout; `"denied"` means the user blocked the prompt → step 7 §5, never §1; `"granted"` with no `id` after the timeout is the real step-2 timeout → step 7 as written. Never send the user to the dashboard or the console to look up the subscription ID while a tool in the session can read it. -- **MCP path:** there is no suitable MCP tool for a presence poll — `export_subscriptions_csv` is a whole-audience async export (capped at 1 concurrent run per account), and `estimate_recipient_count` is email-only and returns 0 for push. Use the page read above (web) or the REST poll below (a key is still required). If only the MCP is available, no key, and no browser tool: tell the user this rung needs a key, and offer one alternative through the structured-question tool — the ID from the debug helper's console line `[OneSignal] Push subscription registered: `. The page is already open in their browser from step 1 (the OS opener), so the ask starts at the Allow click, not at "open this URL"; name that exact line and where the console is; do not send them to the dashboard for it. +- **MCP path:** there is no suitable MCP tool for a presence poll — `export_subscriptions_csv` is a whole-audience async export (capped at 1 concurrent run per account), and `estimate_recipient_count` is email-only and returns 0 for push. Use the page read above (web) or the REST poll below (a key is still required). If only the MCP is available, no key, and no browser tool: tell the user this rung needs a key, and offer one alternative through the structured-question tool — the ID from the debug helper's console line `[OneSignal] Push subscription registered: `. If the OS opener ran in step 1, the page is already in their browser, so the ask starts at the Allow click, not at "open this URL"; if the opener fell back, include the URL. Name that exact line and where the console is; do not send them to the dashboard for it. - **REST path:** `GET https://api.onesignal.com/players?app_id=&limit=1` with `Authorization: Key `. **Note: `/players` is the legacy Devices API — documented but marked deprecated** ("View players" reference page). Its documented auth form is `Authorization: Basic `; try `Key` first with the current key and fall back to `Basic` if rejected. If the endpoint errors entirely, fall back to the dashboard (Audience → Subscriptions) and have the user confirm the row appeared. The only thing you assert from this call is *non-empty ⇒ a subscriber registered*. - **Poll loop:** every ~5s, up to a **2-minute timeout**. Between polls: if you launched the app yourself (iOS simulator), tail the launch's `console.log` (the run step prints its path — platform-verification.md Part A) for the debug helper's registration line instead of nagging; if you opened the web page in a browser tool, re-read the page values; if you opened it with the OS opener, the tab is already in front of the user, so remind them of the Allow click only, and do not reopen the page; otherwise remind the user to launch the app on a real device/browser and accept the notification permission prompt. - **Read `notification_types` on the returned player — presence alone is not opt-in.** `notification_types >= 1` means registered AND opted in — the activation ladder's bar (platform-matrix.md). A row with `notification_types < 1` on iOS is the normal pre-tap state (the device registers before the prompt is answered — see step 1): report "registered — waiting on the permission tap", ask the user for the tap, and keep polling until the value reaches `>= 1`. **Do not run step 4 against a target below 1** — that send errors as unsubscribed, and step 7 §1 would misread it as a credentials gap. The script reports this as `first_subscription_opted_in`. diff --git a/skills/verify/platform-verification.md b/skills/verify/platform-verification.md index 9da4c71..a121bd3 100644 --- a/skills/verify/platform-verification.md +++ b/skills/verify/platform-verification.md @@ -18,7 +18,12 @@ The goal of the gate is only to get the app *running with the SDK linked* so the - **Platform-provisioned probe (run FIRST — free, no auth):** `curl -s "https://api.onesignal.com/sync//web?fresh=$(date +%s)"`. `success: true` ⇒ the web platform is configured. `{"success":false,"code":2,"description":"This app is not configured for web push."}` ⇒ the dashboard web-platform step never happened — go straight to Part B §2 first bullet; nothing else can pass until it's fixed. Always include the throwaway query param: responses are CDN-cached ~1 h (`Cache-Control: public, max-age=3600`, `Vary: Origin`), so the plain URL can serve a stale answer. - **Pass condition:** sync probe returns `success: true`; page loads; the init script tag is present; **`OneSignalSDKWorker.js` is reachable same-origin** — open `https:///OneSignalSDKWorker.js` (or the configured path) and confirm it returns the one-line `importScripts(...)` body with `Content-Type: application/javascript`, no redirect. A 404/403/redirect/wrong-MIME here is the most common web failure once the platform is provisioned (Part B §2). - **Open the page yourself when the session exposes a browser tool** (SKILL.md "Inputs you need"): load the served URL in it, then read `OneSignal.User.PushSubscription.id` and `.optedIn` from the page for step 2 — no REST key needed. When the setup skill wrote the integration, its debug helper opens the OneSignal slidedown about 2 seconds after init; the user clicks Allow on the slidedown and then in the browser's native dialog. Tell them that once, then read the values. The native dialog is outside the page, so no tool clicks it. -- **Without a browser tool, open the page in the user's default browser yourself** before you ask them for anything: Vite and most dev servers do not open a tab on their own, and a user who sees only a URL in the chat has to notice it and open it. Run the OS opener on the served URL — macOS `open `, Linux `xdg-open `, Windows `start "" ` (`Start-Process ` in PowerShell). Each opens a new tab, which is what the SDK needs (a new tab runs the full init; a refresh does not always). Then tell the user once: the tab is open, click Allow on the slidedown and in the browser's native dialog. If the opener is missing, exits non-zero, or the host blocks the command (a sandboxed runtime can), fall back to the instruction: open a fresh tab at ``, click Allow on the slidedown and in the native dialog. Either way, poll step 2 next. Open the page once per run; do not reopen it on every poll. +- **Without a browser tool, open the page in the user's default browser yourself** before you ask them for anything: Vite and most dev servers do not open a tab on their own, and a user who sees only a URL in the chat has to notice it and open it. Run the OS opener on the served URL, once per run, after the pass condition above holds: + - macOS: `open ` — or `open -a "Google Chrome" ` when the URL is a `localhost` origin and Chrome is installed. + - Linux: `xdg-open `. + - Windows: `cmd /c start "" ` — the one form that works from `cmd.exe`, PowerShell, and Git Bash; the `""` is the window-title argument that `start` consumes first. + + Open only an `http://` or `https://` URL that the dev server printed. Each command opens a new tab, which the SDK needs: a new tab runs the full init, a refresh does not. Then tell the user once: the tab is open, click Allow on the slidedown and in the browser's native dialog. **When the URL is a `localhost` origin, add one sentence:** the page must be in Chrome or Edge (the Serve bullet above — only Chromium treats `localhost` as a secure origin for push), so if it opened in Safari or Firefox, copy the URL into Chrome. If the opener is missing, exits non-zero, or the host blocks the command (a sandboxed runtime can), fall back to the instruction: open a fresh tab at ``, click Allow on the slidedown and in the native dialog. Either way, poll step 2 next. Do not reopen the page on every poll. ### Android - **Build:** `./gradlew assembleDebug` (or `:app:assembleDebug`) from the project root. If the wrapper is missing, use the user's documented build command. From adcaed71198dbc64ad80b7796563c825ebbc2d01 Mon Sep 17 00:00:00 2001 From: sherwinski Date: Thu, 22 Oct 2026 13:42:57 -0700 Subject: [PATCH 3/3] docs: match the Windows opener shorthand to the full form Both mentions in the verify skill now say the same command as platform-verification.md, with the empty window-title argument. --- skills/verify/SKILL.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skills/verify/SKILL.md b/skills/verify/SKILL.md index 5814468..0ec9f3d 100644 --- a/skills/verify/SKILL.md +++ b/skills/verify/SKILL.md @@ -78,7 +78,7 @@ Do not ask twice. A refusal is a valid answer: checkpoints stay local, and you n | App ID | Public; from the init code you can grep, or the prior skill's summary | every API step | | App-scoped key | provided by the setup flow / invocation, or env `$ONESIGNAL_REST_API_KEY` / `$ONESIGNAL_SETUP_TOKEN`; if none of these, MCP or the Keys & IDs link below | steps 2, 4–5 (unless MCP) | | MCP connected? | Check for the OneSignal MCP tools (`onesignal_health`, `send_message`, `view_user`, `view_message`); if the tools are absent, check for a registered-but-unauthenticated server before you fall back (see "No key → MCP first" below) | preferred path for 4–5 | -| Browser tool? (web only) | Check the session's tool list for a browser automation tool — for example the Claude in Chrome extension, a Playwright or Chrome DevTools MCP server, or the Cursor browser. Use only a tool the session exposes; never invent the call (safety contract §14a). Without one, the OS opener (`open` / `xdg-open` / `cmd /c start`) still puts the page in front of the user; it cannot read the page | step 1 page load, step 2 page-side subscription read (no key needed) | +| Browser tool? (web only) | Check the session's tool list for a browser automation tool — for example the Claude in Chrome extension, a Playwright or Chrome DevTools MCP server, or the Cursor browser. Use only a tool the session exposes; never invent the call (safety contract §14a). Without one, the OS opener (`open` / `xdg-open` / `cmd /c start ""`) still puts the page in front of the user; it cannot read the page | step 1 page load, step 2 page-side subscription read (no key needed) | | external_id (only if the app wires `login`) | Grep the wrapper for the `login(...)` call | step 3 identity check | **No key → MCP first, Keys & IDs link second.** Resolve in this order — never jump straight to the key ask: @@ -118,7 +118,7 @@ The device cannot register until the app actually runs with the SDK linked. Pick - Run builds via the user's own package manager (detect via lockfile — safety contract §7). Do not add or bump dependencies. - If the build fails, report the compiler error verbatim and STOP. A build break is not a OneSignal problem yet; hand it back or route to the setup skill if the failure is in OneSignal wiring. Do not proceed to API steps against an app that never ran. -- **Run the app yourself when a simulator or a local server can serve.** On an Apple-silicon Mac, boot, install, and launch the iOS simulator yourself — unattended, but not headless: keep the Simulator window open for the user's later permission tap (exact `simctl` sequence in platform-verification.md Part A). On web, when the session exposes a browser tool, open the served page in it yourself; without one, open the served URL in the user's default browser with the OS opener (`open` / `xdg-open` / `cmd /c start`) before you ask them to do anything — dev servers such as Vite do not open a tab on their own (platform-verification.md Part A → Web). Do not ask the user to do a run you can do. A physical phone is different: you cannot drive it, so hand that launch to the user and wait. +- **Run the app yourself when a simulator or a local server can serve.** On an Apple-silicon Mac, boot, install, and launch the iOS simulator yourself — unattended, but not headless: keep the Simulator window open for the user's later permission tap (exact `simctl` sequence in platform-verification.md Part A). On web, when the session exposes a browser tool, open the served page in it yourself; without one, open the served URL in the user's default browser with the OS opener (`open` / `xdg-open` / `cmd /c start ""`) before you ask them to do anything — dev servers such as Vite do not open a tab on their own (platform-verification.md Part A → Web). Do not ask the user to do a run you can do. A physical phone is different: you cannot drive it, so hand that launch to the user and wait. - **You cannot press "Allow" anywhere — simulator included** (`xcrun simctl privacy` has no notifications service). On iOS the prompt does not gate step 2: with the setup skill's `UIBackgroundModes = remote-notification` edit, the SDK creates the server-side subscription before the prompt is answered (SDK-verified; detail in platform-verification.md Part A). The accepted prompt is needed from step 4 on — tell the user the single tap you need from them, and when. If the setup skill did not write this integration, confirm `remote-notification` is in `Info.plist` first: without that background mode, the prompt gates step 2 too, and a step-2 timeout means "tap not done", not "credentials missing". ### Step 2 — Subscription presence (poll until first device registers)