Skip to content
Open
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
4 changes: 3 additions & 1 deletion AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,9 @@ Do not break these without a decision from the team:
single call; a compound command prompts.
- **Shared files have one spelling in skill text.** A skill refers to a shared script only as
`<plugin>/scripts/<name>`, and to a reference only as `[…](../../references/<name>.md)`.
No other path in a skill may contain `../`. `scripts/package_directory_bundle.py --check`
No other path in a skill may contain `../`; a fenced code block is exempt, because its
content goes into the user's project (a pbxproj `rpath`, for example) and is not a link
the plugin resolves. `scripts/package_directory_bundle.py --check`
enforces this convention, and any other spelling fails the check. The convention is what
lets the packager build the self-contained bundle with one rewrite (see "The OpenAI
directory bundle" below).
Expand Down
2 changes: 1 addition & 1 deletion references/platform-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ What the agent can do vs. what stays human, per platform. Verified against SDK r
| **Web** (most automatable) | `<script src="https://cdn.onesignal.com/sdks/web/v16/OneSignalSDK.page.js" defer>` + `OneSignal.init({appId})` via `OneSignalDeferred`; create `OneSignalSDKWorker.js` containing one line `importScripts("https://cdn.onesignal.com/sdks/web/v16/OneSignalSDK.sw.js");` placed in `public/` (maps to origin root on Next/CRA/Vite/Vue/Angular) | Dashboard web-platform config (Site URL must EXACTLY match origin); confirm deployed site serves the worker same-origin over HTTPS with `Content-Type: application/javascript` | **Signup/onboarding does NOT provision the web platform** — until the dashboard step happens the SDK fails init with `App not configured for web push` (sync `code: 2`); probe `GET /sync/{app_id}/web` (api-reference), or provision via the write-once credentials endpoint (`chrome_web_origin`). SW must be same-origin — no CDN/subdomain. Subdir placement needs `serviceWorkerPath` + `serviceWorkerParam.scope`. PWA conflict: only one SW per scope. localhost testing works with a separate app + `allowLocalhostAsSecureOrigin: true`. Frameworks: react-onesignal / onesignal-vue3 / onesignal-ngx npm packages. |
| **Expo** (best mobile) | `npx expo install onesignal-expo-plugin` + `npm i react-native-onesignal`; app.json: plugin FIRST in plugins array, `mode`, `smallIcons`; init in `App.tsx`/`_layout.tsx` | Apple .p8 + Firebase JSON procurement; EAS credentials must match; dev build (push doesn't work in Expo Go) | Config plugin generates NSE, entitlements, App Group, UIBackgroundModes at prebuild — zero manual Xcode. Expo SDK 53+/RN 0.79+ New Architecture. |
| **Android** | Gradle dep `com.onesignal:OneSignal:<exact Stable from releases.json>` (e.g. `5.9.1` — **exact pin, never a version range**); Application subclass with `OneSignal.initWithContext(this, APP_ID)`; register in AndroidManifest `android:name`; `requestPermission` call | Firebase console: create project (if none), generate **service-account JSON**; (agent uploads it via API) | **`google-services.json` is NOT required for OneSignal** — FCM v1 credentials are server-side only (the upstream ai-prompt requiring it is a known bug; only keep it if the app itself uses Firebase client SDKs). `POST_NOTIFICATIONS` is manifest-merged by the SDK — no edit. Play-services emulator OK for testing. **Version + Kotlin floor:** resolve the exact pin with `python3 scripts/resolve_sdk_version.py android` (never compose the line by hand — ranges break the build). The SDK's OpenTelemetry submodule transitively pulls a newer `kotlin-stdlib` than its POM admits (5.9.1 resolves to 2.2.20), so the host needs a Kotlin compiler that can read that metadata — check it with `python3 scripts/android_kotlin_check.py . --deps -` (fed a `:app:dependencies` run) and surface any required toolchain bump as a separately-approved change; never half-bump to a version still below the resolved stdlib. |
| **iOS native** | SPM (`OneSignal-XCFramework` repo, map `OneSignalFramework`→app) or Podfile; `OneSignal.initialize(appId, withLaunchOptions:)` in AppDelegate/adaptor; Info.plist `UIBackgroundModes remote-notification`; entitlements text; debug-only verification helper | Apple Developer portal: paid account, Push capability on App ID, generate **.p8** (+ Key ID/Team ID); Xcode GUI: signing, capabilities toggle, and **NSE target creation** (File>New>Target — not reliably text-editable); physical-device test (launch from Xcode) + the notification permission tap | NSE is OPTIONAL for minimal install (needed only for rich media / confirmed receipt / badges) — OneSignal's own AI prompt skips it. Never hardcode a fallback App ID. Remote push testing: physical device (human launches), or a simulator on an Apple-silicon Mac that the verify skill boots, installs, and launches itself via `simctl` (Xcode 14+ simulators there receive real sandbox APNs pushes; Intel-Mac simulators do not) — only the permission tap stays human. |
| **iOS native** | SPM (`OneSignal-XCFramework` repo — never `OneSignal-iOS-SDK`; `OneSignalFramework` + `OneSignalInAppMessages`→app, `OneSignalExtension`→NSE) or Podfile (`OneSignal/OneSignal` + `OneSignal/OneSignalInAppMessages` on the app target, `OneSignal/OneSignal` on the NSE target); `OneSignal.initialize(appId, withLaunchOptions:)` in AppDelegate/adaptor; Info.plist `UIBackgroundModes remote-notification`; entitlements text (`aps-environment` + App Group); the NSE folder (3 templates) and its pbxproj target objects; debug-only verification helper | Apple Developer portal: paid account, Push capability on App ID, generate **.p8** (+ Key ID/Team ID); Xcode GUI: signing, capabilities toggle, and **NSE target creation** (File>New>Target) only when the pbxproj format does not match the recipe; physical-device test (launch from Xcode) + the notification permission tap | NSE + App Group are part of the default install (images, badges, confirmed receipt fail silently without them); `OneSignalLocation` only on an opt-in (one question before the diff). Never hardcode a fallback App ID. Remote push testing: physical device (human launches), or a simulator on an Apple-silicon Mac that the verify skill boots, installs, and launches itself via `simctl` (Xcode 14+ simulators there receive real sandbox APNs pushes; Intel-Mac simulators do not) — only the permission tap stays human. |
| **React Native / Flutter / Cordova / Capacitor** | Package install (`react-native-onesignal` / `onesignal_flutter` / `onesignal-cordova-plugin` / `@onesignal/capacitor-plugin` + `npx cap sync` — exact Stable version from releases.json, no ranges/carets); init call in entry file; Podfile NSE pod line; version bumps (RN 0.79+ New Arch, Flutter 3.29+) | Everything in the iOS-native human column (these wrappers inherit the full native iOS GUI flow); Firebase JSON procurement | Capacitor: set `ios.handleApplicationNotifications: false` in capacitor.config. Android side is handled by the wrapper plugins — no manual gradle/manifest edits documented. |
| **Unity** | Init `OneSignal.Initialize("APP_ID")` in a MonoBehaviour; icon assets | GUI-bound install (Package Manager/Asset Store), Player Settings gradle-template toggles | Low human friction but low agent-automatability. Unity 2022.3+, Android API 33+. |

Expand Down
6 changes: 3 additions & 3 deletions references/safety-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ Any skill that reads or writes the user's repository MUST follow all of this. It
1. `git status --porcelain -- ":(exclude,top).onesignal"` — if the tree is dirty, STOP and ask (stash / proceed / abort). `.onesignal/` is run state (§20), never project content, so it never makes the tree dirty: the `top` pathspec excludes it at the repo root from any working directory, whether it is untracked or modified. No `.git`? Fall back to `.onesignal.bak` sibling backups and say there's no VCS net.
2. Detect prior installation FIRST: existing OneSignal dependency line, existing init call, or our marker comment. Found → propose update/repair, never a duplicate. Different App ID already present → ask which is correct; never silently overwrite.
3. Default to a new `onesignal-integration` branch (user may opt into current branch).
4. Declare the complete file allow-list up front (dependency manifest, init/lifecycle file, one wrapper module, platform config files, one debug-only verification helper, `.gitignore`, web service-worker, `.onesignal/` checkpoint run state). Touching anything else requires re-confirmation.
4. Declare the complete file allow-list up front (dependency manifest, init/lifecycle file, one wrapper module, platform config files, the iOS Notification Service Extension folder, one debug-only verification helper, `.gitignore`, web service-worker, `.onesignal/` checkpoint run state). Touching anything else requires re-confirmation.

## Writing

5. Compute the FULL change set and show it as diffs BEFORE any write. One confirmation for the whole set. Apply exactly as previewed; if a file drifted, abort that file and re-preview.
6. Mark generated blocks: `// onesignal:managed v1` — idempotency keys off this marker on re-runs.
7. Match the repo's style, package manager (detect via lockfile), and architecture. No repo-wide reformatting, no unrelated dep bumps, no import reordering.
8. Minimal integration only: init + permission in the right lifecycle spot. No extra OneSignal features unless asked.
8. Minimal integration only: init + permission in the right lifecycle spot. No extra OneSignal features unless asked. On native iOS the minimal integration includes the Notification Service Extension, the App Group, and the `OneSignalInAppMessages` product: without the first two, images, badges, and confirmed delivery fail silently, and the third is the documented default with no code of its own. `OneSignalLocation` is an extra feature: the iOS reference asks once, and adds it only on a yes.

## Never

Expand Down Expand Up @@ -48,7 +48,7 @@ The onboarding flow can deliver the app-scoped key inside the invocation (`/ones

## Asking the user (human gates)

14. A human gate (checkpoint consent, dirty-tree stash/proceed/abort, missing App ID, no-VCS backup consent, the pre-write diff confirmation, test-send consent) BLOCKS: do not continue, mutate files, or assume an answer until the user responds. Ask via the harness's native structured-question tool when one exists — Claude Code: `AskUserQuestion`; Codex: `request_user_input` — otherwise ask plainly in chat and end the turn. Call the tool by name; a list of choices written in prose does not render as a selectable prompt. Use structured choices for bounded decisions (stash/proceed/abort, yes/no, pick-a-platform, share-or-keep-local checkpoints). Give each question 2 or 3 options (the Codex limit). When one option is the recommended path, put it first and end its label with "(Recommended)". The checkpoint consent question is the exception: it has no recommended option, and it keeps the order the skill lists, with "Share reports with OneSignal" first. Never add your own "Other" option — both hosts add a free-text "Other" by themselves. Codex in Default mode usually lists only `request_user_input_async`; use it when `request_user_input` is absent. The Codex app shows the async call as a question card with the choices; the Codex CLI shows it as plain text. The async call returns `{"accepted":true}` without the answer, so after it write one plain-text line that names each choice and the exact word to reply with — for example "Reply `share` or `keep`." — and end the turn. If no question tool is listed, or the host rejects the call, write that same plain-text question alone and end the turn. Never pick an answer for the user, and never continue past the gate on `{"accepted":true}`. For free-form values (App ID, Key ID, Team ID, file paths, bundle IDs, URLs), still prefer the structured tool when it accepts typed text (most tools show an "Other" free-text field): the question names the value, the user types it, and every listed option is a fallback route — for example "Help me find it" (repeat the portal location) or "Pause — I'll come back". One question per value, and when several values are pending, batch them as ONE tool call with one entry per value in its `questions` array, at most 3 questions per call (the Codex limit; a longer set goes in consecutive calls) — do not issue parallel single-question calls, which can leave only the last prompt visible — then wait for the answers. Fall back to plain chat only when the tool cannot take free text. Secrets are the exception, always: never make a secret's contents an option or a typed answer — secrets stay file-path-only (see "Never").
14. A human gate (checkpoint consent, dirty-tree stash/proceed/abort, missing App ID, no-VCS backup consent, the iOS location opt-in, the pre-write diff confirmation, test-send consent) BLOCKS: do not continue, mutate files, or assume an answer until the user responds. Ask via the harness's native structured-question tool when one exists — Claude Code: `AskUserQuestion`; Codex: `request_user_input` — otherwise ask plainly in chat and end the turn. Call the tool by name; a list of choices written in prose does not render as a selectable prompt. Use structured choices for bounded decisions (stash/proceed/abort, yes/no, pick-a-platform, share-or-keep-local checkpoints). Give each question 2 or 3 options (the Codex limit). When one option is the recommended path, put it first and end its label with "(Recommended)". The checkpoint consent question is the exception: it has no recommended option, and it keeps the order the skill lists, with "Share reports with OneSignal" first. Never add your own "Other" option — both hosts add a free-text "Other" by themselves. Codex in Default mode usually lists only `request_user_input_async`; use it when `request_user_input` is absent. The Codex app shows the async call as a question card with the choices; the Codex CLI shows it as plain text. The async call returns `{"accepted":true}` without the answer, so after it write one plain-text line that names each choice and the exact word to reply with — for example "Reply `share` or `keep`." — and end the turn. If no question tool is listed, or the host rejects the call, write that same plain-text question alone and end the turn. Never pick an answer for the user, and never continue past the gate on `{"accepted":true}`. For free-form values (App ID, Key ID, Team ID, file paths, bundle IDs, URLs), still prefer the structured tool when it accepts typed text (most tools show an "Other" free-text field): the question names the value, the user types it, and every listed option is a fallback route — for example "Help me find it" (repeat the portal location) or "Pause — I'll come back". One question per value, and when several values are pending, batch them as ONE tool call with one entry per value in its `questions` array, at most 3 questions per call (the Codex limit; a longer set goes in consecutive calls) — do not issue parallel single-question calls, which can leave only the last prompt visible — then wait for the answers. Fall back to plain chat only when the tool cannot take free text. Secrets are the exception, always: never make a secret's contents an option or a typed answer — secrets stay file-path-only (see "Never").

## Guidance loaded from a connected MCP server

Expand Down
19 changes: 13 additions & 6 deletions references/telemetry-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,16 @@ Each is `skill.milestone`. Status is `ok`, `ok_after_fix`, or `fail`.
`platform` field separates them. It fires once the Step-5 change set and the Step-6
verification helper both exist, because the prerequisites span both steps:

- `ios` — the capability set and the permission request: `UIBackgroundModes`
- `ios` — the capability set, the permission request, and the extension: `UIBackgroundModes`
`remote-notification`, the `aps-environment` entitlement, the pbxproj build settings,
and the `requestPermission` call in the verification helper. When the pbxproj format
does not match and the capability toggle goes to the human in Xcode, report
`fail capability_manual` at the hand-off and continue. Like `deferred` on the
the `requestPermission` call in the verification helper, the
`OneSignalNotificationServiceExtension` target with `OneSignalExtension` linked, and the
same App Group in both entitlements files. `ok` needs the structural self-check
(`verify_integration.py --platform ios`) to pass `ios_nse_present` and
`ios_app_group_matches`, not only the agent's own reading of the edits. When the
pbxproj format does not match and the capability toggle or the NSE target creation
goes to the human in Xcode, or when one of those 2 rows still fails after a repair,
report `fail capability_manual` at the hand-off and continue. Like `deferred` on the
credentials gate, this records a drop out of agent automation, not a skill stop.
- `android` — the permission state: `INTERNET` present in the manifest, no manual
`POST_NOTIFICATIONS` line (the SDK manifest-merges it), and the `requestPermission`
Expand Down Expand Up @@ -195,8 +200,10 @@ this; android.md documents the exact dependency line). Sibling of `buildconfig_d
correct code that does not compile until the app module declares one more thing.

`capability_manual`: the iOS pbxproj edits could not apply safely, so the Push
Notifications and Background Modes toggles went to the human in Xcode. The run cannot
observe when the human completes them, so the milestone records the hand-off itself.
Notifications and Background Modes toggles, or the creation of the Notification Service
Extension target, went to the human in Xcode. The same class covers an edit that applied
but still fails the extension or App Group row of the self-check after a repair. The run
cannot observe when the human completes them, so the milestone records the hand-off itself.

`worker_scope_conflict`: the site already registers a service worker at the scope
OneSignal needs. `setup.platform_config` reports the resolved conflict — the combine or
Expand Down
Loading
Loading