Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions skills/verify/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` / `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:
Expand Down Expand Up @@ -118,17 +118,17 @@ 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` / `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)

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 <plugin>/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: <id>` 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: <id>`. 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: <id>`. 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=<APP_ID>&limit=1` with `Authorization: Key <KEY>`. **Note: `/players` is the legacy Devices API — documented but marked deprecated** ("View players" reference page). Its documented auth form is `Authorization: Basic <legacy REST API key>`; 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.
Expand Down
7 changes: 6 additions & 1 deletion skills/verify/platform-verification.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<APP_ID>/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://<origin>/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, once per run, after the pass condition above holds:
- macOS: `open <url>` — or `open -a "Google Chrome" <url>` when the URL is a `localhost` origin and Chrome is installed.
- Linux: `xdg-open <url>`.
- Windows: `cmd /c start "" <url>` — the one form that works from `cmd.exe`, PowerShell, and Git Bash; the `""` is the window-title argument that `start` consumes first.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Worth confirming on Windows before relying on the Git Bash claim. Claude Code on Windows runs commands through Git Bash, and MSYS path conversion is known to rewrite a lone /c to C:/, which leaves cmd open as an interactive shell instead of running start. I haven't reproduced it. If it happens, cmd //c start "" <url> or MSYS_NO_PATHCONV=1 avoids it. Also, SKILL.md:81 and :121 give the shorthand cmd /c start without the "", so an agent that stops at SKILL.md runs a different form than this line recommends.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Willing to defer on this for now. I also think agents can understand intent of an instruction, even if the explicit instruction itself may be incorrect.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The 2 shorthand mentions in SKILL.md now say cmd /c start "", the same form as this line (19bd7fe). The MSYS path-conversion question stays deferred.


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 `<url>`, 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.
Expand Down
Loading