From c244923bd7f97c44ff957a97e4f4e954319b0b23 Mon Sep 17 00:00:00 2001 From: Franco Zalamena Date: Thu, 22 Oct 2026 14:12:25 +0100 Subject: [PATCH 01/15] Bring the README in line with the iOS and React Native releases The README still described an Android-only flow in places, used an update command that has no in-session form, and said JWT keys work for core verification on Android, where the key check refuses them. It now covers the iOS setup and host requirements, the React Native order and Release build, the account roles each provider needs, proof side effects, Codex updates, and uninstalling. Co-Authored-By: Claude Opus 5.5 --- .claude-plugin/plugin.json | 2 +- .cursor-plugin/plugin.json | 2 +- README.md | 133 ++++++++++++++++++++++++++++--------- 3 files changed, 104 insertions(+), 33 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 32f4879..9265995 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "iterable-sdk", "version": "26.10.4-beta", - "description": "Takes an Android or iOS app — native or React Native — from nothing to a push notification proven on a device: Firebase or Apple (APNs) provisioning, version-pinned SDK integration, silent-failure pitfalls, and device-side proof that a push actually arrived. Android proof runs on a real device; iOS proof runs on the iOS Simulator.", + "description": "Takes an Android or iOS app — native or React Native — from nothing to a push notification proven on a device: Firebase or Apple (APNs) provisioning, version-pinned SDK integration, silent-failure pitfalls, and device-side proof that a push actually arrived. Android proof runs on a device or emulator; iOS proof runs on the iOS Simulator.", "author": { "name": "Iterable" }, "repository": "https://github.com/Iterable/iterable-sdk-skill", "license": "MIT", diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 405a847..ead3696 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "iterable-sdk", "version": "26.10.4-beta", - "description": "Takes an Android or iOS app — native or React Native — from nothing to a push notification proven on a device: Firebase or Apple (APNs) provisioning, version-pinned SDK integration, silent-failure pitfalls, and device-side proof that a push actually arrived. Android proof runs on a real device; iOS proof runs on the iOS Simulator.", + "description": "Takes an Android or iOS app — native or React Native — from nothing to a push notification proven on a device: Firebase or Apple (APNs) provisioning, version-pinned SDK integration, silent-failure pitfalls, and device-side proof that a push actually arrived. Android proof runs on a device or emulator; iOS proof runs on the iOS Simulator.", "author": { "name": "Iterable" }, "repository": "https://github.com/Iterable/iterable-sdk-skill", "license": "MIT", diff --git a/README.md b/README.md index 4e23193..a8cb05f 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,10 @@ The skills can work independently. A new push integration normally follows: `iterable-provision` → platform integration skill → matching platform verification skill +For a React Native app with both platforms in scope, the workflow finishes +Android first, including a proved push, and then sets up iOS. The iOS setup +reuses the Iterable keys, region, and feature scope from the Android setup. + Skills are model-invoked. Describe the outcome you want rather than running a skill by name. For example: @@ -41,7 +45,7 @@ skill by name. For example: - Native iOS SDK integration. - React Native integration for Android and iOS, including Expo. - Firebase provisioning and device-side push verification for Android. -- Apple/APNs prerequisite guidance and iOS runtime proof. +- Apple/APNs prerequisite guidance and iOS push proof on the iOS Simulator. - Push notifications, in-app messages, mobile inbox, embedded messaging, deep links, JWT authentication, event tracking, and user profiles. - Unknown user activation for native Android and native iOS. @@ -52,16 +56,20 @@ Web SDK integration is not included. ### Accounts -- An Iterable project. The provisioning skill can guide you through creating - the required Iterable API keys and mobile configuration. +- An Iterable project, and an Iterable role that can create API keys and + mobile apps. The provisioning skill can guide you through creating the + required Iterable API keys and mobile configuration. - A Google account with access to a Firebase project, or permission to create - one, when setting up Android push. + one, when setting up Android push. The account must be able to enable APIs, + add Firebase, and create a service account and its key. - Apple Developer Program access to the app's team when setting up native iOS - push. The workflow cannot replace Apple sign-in, 2FA, agreements, or the - one-time download of a newly created APNs private key. + push. Registering the App ID, enabling Push Notifications, and creating an + APNs key require the **Account Holder** or **Admin** role. The workflow + cannot replace Apple sign-in, 2FA, agreements, or the one-time download of a + newly created APNs private key. -You authenticate with Google and Iterable yourself. The plugin does not ask for -or handle your account passwords. +You authenticate with Google, Apple, and Iterable yourself. The plugin does not +ask for or handle your account passwords. ### Command-line tools @@ -78,7 +86,7 @@ xcodebuild -version |---|---|---| | `gcloud` | Google authentication, Firebase projects, service accounts, IAM, and key creation | [Google Cloud CLI](https://cloud.google.com/sdk/docs/install) or `brew install --cask gcloud-cli` | | `adb` | Selecting an Android device and reading the installed app, SDK registration, and arriving notification | Android Studio's SDK Manager or `brew install --cask android-platform-tools` | -| Xcode command-line tools | Building, signing, installing, and observing native iOS apps | Install Xcode, then select it with `xcode-select` | +| Xcode 14 or later | Building, signing, installing, and observing native iOS apps | Install the full Xcode app, not only the Command Line Tools, then select it with `xcode-select` | | Node.js | Parsing command and API output | [nodejs.org](https://nodejs.org) or `brew install node` | The scripts also use Bash, `curl`, and standard Unix utilities. They do not @@ -97,8 +105,13 @@ only one is connected, so the proof is performed on the device you intend to test. For native iOS, development push can be proved on an eligible booted iOS 16+ -Simulator or a signed physical iPhone. Production/TestFlight proof requires a -physical iPhone. +Simulator or a signed physical iPhone. A Simulator receives real APNs sandbox +pushes only on macOS 13 or later with Xcode 14 or later, on a Mac with Apple +silicon or a T2 chip. Production/TestFlight proof requires a physical iPhone. + +React Native and Expo apps are proved on iOS with a Release build, which embeds +the JavaScript bundle and runs without Metro. Expo apps need a development +build (`expo-dev-client`); Expo Go cannot load the SDK's native module. During native iOS push setup, the developer chooses whether to reuse an existing Apple configuration, follow a guided Account Holder or Admin @@ -115,11 +128,10 @@ one APNs sandbox probe. No extra tools are needed beyond Node, curl, and Xcode. /plugin install iterable-sdk@iterable ``` -Start a new Claude Code session after installation. Skills are loaded when a -session starts. +Run `/reload-plugins`, or start a new Claude Code session, to load the skills. To receive updates automatically, open `/plugin`, select **Marketplaces**, -select **iterable**, and enable auto-update. +select **iterable**, and select **Enable auto-update**. ### Cursor @@ -134,7 +146,8 @@ for skill in iterable-provision iterable-android iterable-android-verify iterabl done ``` -Run **Developer: Reload Window**, then start a new Agent chat. +Keep the clone in place, because the skills run scripts from it. Run +**Developer: Reload Window**, then start a new Agent chat. ### Codex @@ -155,15 +168,16 @@ codex plugin add iterable-sdk@iterable ## What happens during Android push setup -1. The integration skill inspects the application and adds the required SDK - configuration. +1. The integration skill inspects the application and agrees the integration + scope with you. 2. If Firebase or Iterable prerequisites are missing, the provisioning skill explains what it needs to read and asks for permission before continuing. 3. The workflow creates or selects the Firebase resources and service account. 4. It guides you through the Iterable dashboard actions that do not have a - public API: API keys, the mobile app, and the Firebase push integration. -5. Your coding assistant builds and installs the application using the - project's existing toolchain. + public API: API keys, the mobile app, the Firebase push integration (with + FCM type **Data notifications**), and the test user's email address. +5. The integration skill adds the SDK configuration, and your coding assistant + builds and installs the application using the project's existing toolchain. 6. The workflow opens the selected application's launcher activity. You sign in as the test user and respond to any Android permission prompts. 7. The verification skill sends a proof notification and checks Android's @@ -173,10 +187,36 @@ The component that performs an action does not verify its own result. For example, the sender records which proof notification it sent, while a separate read-only verifier checks the device for that notification. +## What happens during iOS push setup + +1. The integration skill inspects the application and agrees the scope and the + user identity with you, once. Provisioning reuses these answers. +2. If Apple prerequisites are missing, the provisioning skill guides you through + Apple Developer one step at a time: the App ID, Push Notifications, and the + APNs key. It checks the APNs key with one sandbox probe. +3. It guides you through the Iterable Mobile and Server-side API keys and the + iOS app with its APNs credential, and checks the keys with live calls. +4. The integration skill writes the SDK code. Your coding assistant builds the + app and installs it on the Simulator. +5. You sign in to the app and allow notifications. +6. The verification skill confirms that Iterable holds the Simulator's device + token, sends one marked proof notification, and reads its arrival from the + Simulator's own notification records. + +If the app already sends its own push notifications, the integration skill asks +whether to keep them alongside Iterable's or use Iterable notifications only. +Iterable's proof completes the integration; checking the app's own +notifications afterwards is optional. + ## Security and data handling - The repository's scripts run locally and make authenticated HTTPS requests - to Google, Firebase, and Iterable. + to Google, Firebase, Apple, and Iterable. +- Push proof creates or updates one proof template in your Iterable project and + sends it only to the test user. The test identity becomes a real user profile + in that project. +- The plugin also adds the Context7 MCP server (no API key) for documentation + lookups and, in Claude Code, five helper subagents. - You authenticate in your own browser or `gcloud` session. The scripts do not collect account passwords or attempt to bypass sign-in, CAPTCHA, or terms-of-service screens. @@ -201,8 +241,9 @@ organization's configuration. Install plugins only from sources you trust. the documented environment limits. - Creating Iterable API keys, mobile apps, and push integrations requires dashboard interaction because these actions do not have public APIs. -- Core verification can run for a JWT-enabled integration, but the app's JWT - backend behavior requires separate feature-specific evidence. +- Android push proof needs a Mobile API key with **Require JWT** turned off. On + iOS, core verification can run for a JWT-enabled integration, but the app's + JWT backend behavior requires separate feature-specific evidence. - Some bundled source articles cover multiple mobile platforms and may include snippets that are not relevant to the current application. - These skills focus on SDK integration. To work with campaign or user data, @@ -211,15 +252,24 @@ organization's configuration. Install plugins only from sources you trust. ## Updating -For Claude Code, update the marketplace before updating the plugin: +For Claude Code, update the marketplace before updating the plugin. Run these +commands in a terminal: -```text -/plugin marketplace update iterable -/plugin update iterable-sdk@iterable +```bash +claude plugin marketplace update iterable +claude plugin update iterable-sdk@iterable ``` -Start a new session after updating. With auto-update enabled, Claude Code may -ask you to reload plugins after a newer version is downloaded. +Then run `/reload-plugins`, or start a new session. With auto-update enabled, +Claude Code may ask you to reload plugins after a newer version is downloaded. + +For Codex: + +```bash +codex plugin marketplace upgrade iterable +``` + +Then start a new Codex session. For Cursor's clone-and-link installation: @@ -228,7 +278,28 @@ cd ~/iterable-skills git pull ``` -Then reload the Cursor window and start a new Agent chat. +Then run the link loop again so that new skills are linked, reload the Cursor +window, and start a new Agent chat. + +## Uninstalling + +```bash +# Claude Code (add --scope project or --scope local if you installed there) +claude plugin uninstall iterable-sdk@iterable +claude plugin marketplace remove iterable + +# Codex +codex plugin remove iterable-sdk@iterable +codex plugin marketplace remove iterable +``` + +For Cursor's clone-and-link installation, delete the links in +`~/.cursor/skills/` and the `~/iterable-skills` clone. + +Uninstalling the plugin leaves cloud resources in place. Before you uninstall, +run `.iterable/teardown` from your project to remove the Google service account, +its role binding and keys, and the local artifacts that Android setup created. +Apple resources and your APNs key are not removed. ## Contributing and architecture @@ -241,4 +312,4 @@ Engineering references: - [`bin/README.md`](bin/README.md) — onboarding program architecture. - [`INTENDED-FLOW.md`](INTENDED-FLOW.md) — authoritative onboarding sequence. -- [`REVIEW.md`](REVIEW.md) — generated SDK documentation review. +- [`REVIEW.md`](REVIEW.md) — how to review refreshed SDK reference docs. From d168af0d495ccbc8af758aafcf26079942726762 Mon Sep 17 00:00:00 2001 From: Franco Zalamena Date: Thu, 22 Oct 2026 14:22:08 +0100 Subject: [PATCH 02/15] Drop the confidentiality notice from the public README Co-Authored-By: Claude Opus 5.5 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a8cb05f..9d32ab4 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Iterable SDK skills > **Private beta.** Use of these skills is subject to Iterable's -> [Beta Terms](https://iterable.com/legal/beta-terms/). This documentation is -> confidential, may contain inaccuracies, and may change during the beta. +> [Beta Terms](https://iterable.com/legal/beta-terms/). This documentation may +> contain inaccuracies and may change during the beta. Use these skills with Claude Code, Cursor, or Codex to integrate Iterable's native Android, native iOS, or React Native SDK. The workflows gather and prove From 7a3d93f7ffcb0e25baf45ea027148bb608b9cf15 Mon Sep 17 00:00:00 2001 From: Franco Zalamena Date: Thu, 22 Oct 2026 14:25:58 +0100 Subject: [PATCH 03/15] Install the Cursor plugin from GitHub, with linking as the fallback Cursor imports skills from a repository as a plugin (Customize, From GitHub Repository), which also brings the scripts and the Context7 MCP server. Linking skill directories stays as a fallback without the MCP server. Co-Authored-By: Claude Opus 5.5 --- README.md | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9d32ab4..0fb6a08 100644 --- a/README.md +++ b/README.md @@ -135,8 +135,31 @@ select **iterable**, and select **Enable auto-update**. ### Cursor -Cursor requires version 3.9 or later. Clone the repository and link the skill -directories: +Cursor requires version 3.9 or later. Install the skills as a plugin: + +1. Open **Customize** in the sidebar. +2. Select **+ Add**, then **From GitHub Repository**. +3. Enter `https://github.com/Iterable/iterable-sdk-skill`. +4. Find **iterable-sdk**, select **Install**, and choose the project or user + scope. + +The plugin installs the skills, the scripts they run, and the Context7 MCP +server. Team admins can also add the repository as a team marketplace. + +To test a local checkout, clone it into Cursor's local plugin folder: + +```bash +git clone https://github.com/Iterable/iterable-sdk-skill.git ~/.cursor/plugins/local/iterable-sdk +``` + +Run **Developer: Reload Window**, then confirm the skills in **Customize**. +Cursor skips a symlink in that folder that points to a clone elsewhere. On +Teams and Enterprise plans, an admin must allow local plugin imports. + +#### Fallback: link the skill directories + +If you can't install plugins, clone the repository and link the skill +directories. This installs the skills without the Context7 MCP server: ```bash git clone --depth 1 https://github.com/Iterable/iterable-sdk-skill.git ~/iterable-skills @@ -271,7 +294,9 @@ codex plugin marketplace upgrade iterable Then start a new Codex session. -For Cursor's clone-and-link installation: +For Cursor, manage the installed plugin from **Customize**. For a local +plugin checkout, run `git pull` in `~/.cursor/plugins/local/iterable-sdk` and +reload the window. For the clone-and-link fallback: ```bash cd ~/iterable-skills @@ -293,8 +318,10 @@ codex plugin remove iterable-sdk@iterable codex plugin marketplace remove iterable ``` -For Cursor's clone-and-link installation, delete the links in -`~/.cursor/skills/` and the `~/iterable-skills` clone. +For Cursor, uninstall the plugin from **Customize**, or delete +`~/.cursor/plugins/local/iterable-sdk` for a local checkout. For the +clone-and-link fallback, delete the links in `~/.cursor/skills/` and the +`~/iterable-skills` clone. Uninstalling the plugin leaves cloud resources in place. Before you uninstall, run `.iterable/teardown` from your project to remove the Google service account, From 6b03ff65f4ba9a241ef6bf82e64a993c5b5caf28 Mon Sep 17 00:00:00 2001 From: Franco Zalamena Date: Thu, 22 Oct 2026 14:21:11 +0100 Subject: [PATCH 04/15] Drop internal ticket IDs and trial anecdotes from shipped skill text The skills are read by outside developers and their agents. Internal Jira keys mean nothing to them, and "in our trial" / "proven live" anecdotes read as war stories rather than guidance. Each passage now states the rule or the observed behaviour directly, and the Android key-name example is neutral. Co-Authored-By: Claude Opus 5.5 --- iterable-android/PITFALLS.md | 2 +- iterable-android/SKILL.md | 9 ++++----- iterable-ios/PITFALLS.md | 6 +++--- iterable-provision/SKILL.md | 4 ++-- iterable-provision/reference/ios.md | 6 +++--- iterable-react-native/PITFALLS.md | 19 ++++++++++--------- iterable-react-native/SKILL.md | 14 +++++++------- 7 files changed, 30 insertions(+), 30 deletions(-) diff --git a/iterable-android/PITFALLS.md b/iterable-android/PITFALLS.md index 303feda..02974e1 100644 --- a/iterable-android/PITFALLS.md +++ b/iterable-android/PITFALLS.md @@ -221,7 +221,7 @@ hot-path subset; the full list lives here and is loaded on demand. load that file **explicitly** — this is the part that's easy to miss: **`local.properties` is NOT exposed as Gradle project properties.** A project's existing `getPropertyIfDefined`/`hasProperty`-style helper (or the - `OPEN_EXCHANGE_RATES_API_KEY` pattern many repos already have) *looks* like + `SOME_SERVICE_API_KEY` pattern many repos already have) *looks* like it reads `local.properties` but does not — it only sees `gradle.properties` / `-P` flags, so it silently yields an empty key. Read the file yourself with `Properties()`. diff --git a/iterable-android/SKILL.md b/iterable-android/SKILL.md index bc6cb7f..ce138cc 100644 --- a/iterable-android/SKILL.md +++ b/iterable-android/SKILL.md @@ -262,11 +262,10 @@ When blocked on any of these, **surface it to the developer and pause that part of the work** — don't silently degrade the integration to keep compiling. **A green build is not the deliverable, and this is the way it actually fails.** -Observed in a live demo: the Firebase project had no Android app registered, so there -was no real `google-services.json`. The push work should have stopped there. Instead -it kept going, made the build pass, knew the Firebase half was unfinished, and then -ended the turn without naming a way forward. Two rules from that, and they are not -negotiable: +The typical case: the Firebase project has no Android app registered, so there is no +real `google-services.json`. The push work should stop there. The failure is to keep +going, make the build pass, know the Firebase half is unfinished, and then end the +turn without naming a way forward. Two rules, and they are not negotiable: - **A missing prerequisite stops the push work, immediately** — before the Gradle edit, not after. Do the parts of the agreed scope that genuinely don't depend on it diff --git a/iterable-ios/PITFALLS.md b/iterable-ios/PITFALLS.md index 4c5a240..eb18bed 100644 --- a/iterable-ios/PITFALLS.md +++ b/iterable-ios/PITFALLS.md @@ -304,9 +304,9 @@ Benchmark-app policy, not a general rule: [`eval/scenarios/ios-benchmark.md`](.. - **Cause:** The App ID does not have Push Notifications enabled on the team (or the APNs key is restricted to other Bundle IDs). APNs then refuses every push for the app with `400 TopicDisallowed`, whichever key signs it, and - Iterable does not surface that refusal. Proven live: two different keys on the - same team both got `TopicDisallowed` before Push was enabled on the App ID, - and both got `200` after. + Iterable does not surface that refusal. Swapping the APNs key does not help: + every key on the team gets `TopicDisallowed` until Push is enabled on the App + ID, and `200` after. - **Fix:** Run `ios-verify check-apple`, which asks Apple directly with one silent push and names the cause. In Apple Developer → Identifiers, check the team selector, open the App ID, tick Push Notifications, Save, and Confirm; diff --git a/iterable-provision/SKILL.md b/iterable-provision/SKILL.md index 4e6aab4..47ea25f 100644 --- a/iterable-provision/SKILL.md +++ b/iterable-provision/SKILL.md @@ -176,8 +176,8 @@ a question* says: `context` above the selector, verbatim, then the options. This ordering is the point and it is easy to get wrong. A status read runs the whole eighteen-gate ladder against their Firebase projects, so a question carried in its output -arrives *after* the work it was asking about. Twice now an agent decided provisioning was -needed, ran it, and presented a project picker — and a project picker is not a consent +arrives *after* the work it was asking about. The usual mistake is to decide provisioning +is needed, run it, and present a project picker — and a project picker is not a consent screen. **Deciding that setup is needed is not permission to start it.** Announcing what you are about to do is not either. diff --git a/iterable-provision/reference/ios.md b/iterable-provision/reference/ios.md index d98569e..dad180d 100644 --- a/iterable-provision/reference/ios.md +++ b/iterable-provision/reference/ios.md @@ -556,13 +556,13 @@ Keep proof levels distinct: - the dashboard's direct Test Push is optional diagnosis when identity association needs to be separated from the APNs path. -Simulator pitfalls seen in live runs: +Simulator pitfalls: - A Simulator needs the registered App ID too, with Push Notifications enabled. For a never-registered Bundle ID, or one without Push, APNs answers `400 TopicDisallowed` for every key on the team; Iterable still returns HTTP - 200 for the send, so nothing arrives and nothing says why (proven live: two - keys both refused before Push was ticked, both accepted after). + 200 for the send, so nothing arrives and nothing says why. Swapping the APNs + key does not help: every key is refused until Push is ticked. `ios-verify check-apple` asks Apple directly and names this. `DeviceTokenNotForTopic` instead means the push targeted a different Bundle ID than the app that registered the token. diff --git a/iterable-react-native/PITFALLS.md b/iterable-react-native/PITFALLS.md index c66d38b..24db45c 100644 --- a/iterable-react-native/PITFALLS.md +++ b/iterable-react-native/PITFALLS.md @@ -123,11 +123,11 @@ iOS (on Expo, #14). Concrete gaps still open on 3.2.1: - - **SDK-594:** `getUnreadInboxMessagesCount` is on the TurboModule spec and + - **Unread inbox count:** `getUnreadInboxMessagesCount` is on the TurboModule spec and implemented natively. `IterableInAppManager` exposes `getMessages` / `getInboxMessages` / `showMessage` / `setReadForMessage` and **no** unread-count wrapper. - - **SDK-593:** `setDeviceAttribute` is used internally to stamp + - **Device attributes:** `setDeviceAttribute` is used internally to stamp `reactNativeSDKVersion` at init. It is not on the TurboModule spec and not on `Iterable`. - `logout` is **not** a Spec method. The public call is `Iterable.logout()` @@ -135,9 +135,10 @@ iOS (on Expo, #14). `setEmail(null)` and `setUserId(null)`). - **Fix:** Only import from `@iterable/react-native-sdk`. Do not import `NativeRNIterableAPI`, `RNIterableAPI`, or `IterableApi`. Do not invent - wrappers for spec-only methods. Until SDK-594 ships, derive an unread - count from `Iterable.inAppManager.getInboxMessages()` (filter unread) - rather than calling the native module. Native configuration (FCM, APNs, + wrappers for spec-only methods. Until the SDK adds an unread-count + wrapper, derive an unread count from + `Iterable.inAppManager.getInboxMessages()` (filter unread) rather than + calling the native module. Native configuration (FCM, APNs, `POST_NOTIFICATIONS`) is still required; it is not a JS method call. See `reference/overview.md` and the feature slug for the task. @@ -324,8 +325,8 @@ the Xcode 27 guidance — the generated `reference/` corpus does not carry it. - **Bare:** in the host `Podfile`, after `react_native_post_install`, raise every Pods target — resource bundles included — that is unset or below **15.1**, then re-run `pod install`. **15.0** is the Xcode 27 floor; - **15.1** matches React Native's own minimum and Iterable's bare RN demo, so - nothing is left at exactly 15.0: + **15.1** matches React Native's own minimum, so nothing is left at exactly + 15.0: ```ruby post_install do |installer| @@ -341,8 +342,8 @@ the Xcode 27 guidance — the generated `reference/` corpus does not carry it. Keep any existing `{fmt}` C++17 workaround (pitfall #11); this lift is separate. - - **Expo SDK 58:** a fresh `prebuild` left no Pods target below 15.0 in our - trial, so no lift is needed. Check rather than assume: + - **Expo SDK 58:** a fresh `prebuild` normally leaves no Pods target below + 15.0, so no lift is needed. Check rather than assume: `grep -h IPHONEOS_DEPLOYMENT_TARGET ios/Pods/Pods.xcodeproj/project.pbxproj | sort -u`. - **Older Expo SDKs:** if that check shows a target below 15.0, lift it in an **Expo config plugin** at `prebuild` — never by committing `ios/Podfile` or diff --git a/iterable-react-native/SKILL.md b/iterable-react-native/SKILL.md index 87eab8c..a4a8cb9 100644 --- a/iterable-react-native/SKILL.md +++ b/iterable-react-native/SKILL.md @@ -258,10 +258,10 @@ everything". **Never fabricate a prerequisite to make the build pass.** Do not invent an API key, a placeholder `google-services.json`, or a JWT signed in the client. -**A green build is not the deliverable.** Seen live on the Android side: the Firebase -project had no app registered, so there was no real `google-services.json`; the work -carried on anyway, made it compile, and ended the turn without naming a way forward. -So — a missing prerequisite stops the push work *before* the native edit, and you say +**A green build is not the deliverable.** The typical failure: the Firebase project has +no app registered, so there is no real `google-services.json`, and the work carries on +anyway, makes it compile, and ends the turn without naming a way forward. Instead, a +missing prerequisite stops the push work *before* the native edit, and you say which parts of the agreed scope you did not do and why. Then route into `iterable-provision` and conduct it here rather than handing over a command: its wizard offers to register the missing app, and `next.question` gives you each screen to put to @@ -432,7 +432,7 @@ Expo). Rule 8 applies whenever the app already sends push of its own. the internal `IterableApi` class. Do not copy Kotlin `IterableApi.getInstance()`, `IterableConfig.Builder()`, or Swift `IterableAPI` into JS. Device attributes and unread-inbox count exist - natively but are not on that facade (pitfall #4, SDK-593, SDK-594). This + natively but are not on that facade (pitfall #4). This package at 3.2.1 ships Android `3.10.1` / iOS `6.7.5` — native APIs newer than those are not reachable (pitfall #5). @@ -458,8 +458,8 @@ Expo). Rule 8 applies whenever the app already sends push of its own. gitignored and regenerated, so a config or plugin change needs a fresh prebuild. On Xcode **27**, use CLI **20.2.0+** (bare) or Expo SDK **58** for Device Hub launch; bare apps also need a `Podfile` lift of Pods - resource-bundle deployment targets, while an Expo SDK 58 prebuild needed none - in our trial — check, don't assume (pitfall **#12**). Never hand-edit + resource-bundle deployment targets, while an Expo SDK 58 prebuild normally needs + none — check, don't assume (pitfall **#12**). Never hand-edit `Pods/`. 8. **If the app already sends push, forward to Iterable — never replace what's From 05f904c6f39e1a7597c4c005097f6603d2eb69a0 Mon Sep 17 00:00:00 2001 From: Franco Zalamena Date: Thu, 22 Oct 2026 14:21:58 +0100 Subject: [PATCH 05/15] Key the provisioning hand-back on install_app, not done next_action reports done only once a push has reached the device, which cannot happen before the platform skill writes the integration. After the Google and Iterable gates pass, the next kind is install_app (or run_app if the app is already installed), so a skill waiting for done never handed over. All three skills now name the state the tool actually reports, and the artifact that is on disk by then. Co-Authored-By: Claude Opus 5.5 --- iterable-android/SKILL.md | 5 +++-- iterable-provision/SKILL.md | 9 ++++++--- iterable-react-native/SKILL.md | 4 +++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/iterable-android/SKILL.md b/iterable-android/SKILL.md index ce138cc..02b07bd 100644 --- a/iterable-android/SKILL.md +++ b/iterable-android/SKILL.md @@ -308,8 +308,9 @@ To check it is installed: walk up from this `SKILL.md` to the first directory containing a `bin/agent`. If there isn't one, this Preflight stops as written below, and the table of dashboard paths is what you give them. -Once provisioning reports `done`, **you own copying the file into the app -module**: `.iterable/artifacts/google-services.json` → `app/google-services.json`, +Once provisioning's `next.kind` reaches `install_app` (or `run_app`) — the Google +and Iterable halves are green and `.iterable/artifacts/google-services.json` exists — +**you own copying the file into the app module**: `.iterable/artifacts/google-services.json` → `app/google-services.json`, shown as part of your one confirmed diff. Nothing else writes into their source tree. diff --git a/iterable-provision/SKILL.md b/iterable-provision/SKILL.md index 47ea25f..442cc58 100644 --- a/iterable-provision/SKILL.md +++ b/iterable-provision/SKILL.md @@ -342,7 +342,7 @@ relay the block, do not look for a flag that gets you past it, and never set | `project_unreachable` | Relay `next.summary` verbatim — it is a permissions answer from Google, not something to work around — and then ask `next.question`, which offers aiming somewhere else. A typo'd project id and a project they have no access to look identical from here, and both are answered at that screen. | | `register_app` | Their project has no Android app for this package. Ask `next.question` — this is the one change the limits promise never happens unless they ask outright, so its yes is separate from the provisioning yes and recorded with `/bin/agent approve create-app`. Never `CREATE_APP=1`. | | `iterable_keys` | Four steps in their Iterable account that only they can do. Ask `next.question`, then walk them **one at a time** — see *The four Iterable steps* below. The two keys reach the workspace with `iterable-keys --take server\|mobile`, off their clipboard; never into this conversation. | -| `install_app` / `run_app` | The Android device half—hand off to `iterable-android-verify`. Until a device is on record `next.question` is the device picker, worth asking even when only one is attached: a phone left plugged in to charge is "the only device", takes the proof push, and the developer watching an emulator reports that nothing arrived. The verifier may open the selected app after installation; signing in and allowing notifications still happen inside their app, by their hand. | +| `install_app` / `run_app` | The Google and Iterable halves are green. If the app has no Iterable integration yet, continue as *Where this goes next* says first. Otherwise this is the Android device half—hand off to `iterable-android-verify`. Until a device is on record `next.question` is the device picker, worth asking even when only one is attached: a phone left plugged in to charge is "the only device", takes the proof push, and the developer watching an emulator reports that nothing arrived. The verifier may open the selected app after installation; signing in and allowing notifications still happen inside their app, by their hand. | | `send_proof` | The Android device half—hand off to `iterable-android-verify`. | | `done` | A real push reached the device. Say which device and chain onward. | @@ -572,8 +572,11 @@ Both are human steps, so they are the two most worth saying out loud: ## Where this goes next -When `next.kind` is `done` for the Google and Iterable halves, announce the -transition in one line and continue into the platform skill — `iterable-android` or +When `next.kind` reaches `install_app` — or `run_app`, if their app is already +installed — the Google and Iterable halves are green (G0–G12) and +`.iterable/artifacts/google-services.json` exists. Do not wait for `done`: that means a +push reached the device, which cannot happen before the integration is written. Announce +the transition in one line and continue into the platform skill — `iterable-android` or `iterable-react-native` — to write the integration. Do not ask whether to continue. The platform skill owns copying `.iterable/artifacts/google-services.json` into diff --git a/iterable-react-native/SKILL.md b/iterable-react-native/SKILL.md index a4a8cb9..9322e8c 100644 --- a/iterable-react-native/SKILL.md +++ b/iterable-react-native/SKILL.md @@ -289,7 +289,9 @@ is installed, walk up from this `SKILL.md` to the first directory containing a else. Let it, and don't run `/bin/agent` or `discover` yourself to save a step — deciding provisioning is needed is not permission to begin it. -Once it reports `done`, **you own placing the file**: on bare RN copy +Once its `next.kind` reaches `install_app` (or `run_app`) — the Google and Iterable +halves are green and `.iterable/artifacts/google-services.json` exists — **you own +placing the file**: on bare RN copy `.iterable/artifacts/google-services.json` to `android/app/google-services.json`; on Expo put it where `expo.android.googleServicesFile` points, never by hand-editing `android/`. Show it as part of your one confirmed diff. From 3ed954ce526301c6e626de795ea4b8e019aabc98 Mon Sep 17 00:00:00 2001 From: Franco Zalamena Date: Thu, 22 Oct 2026 14:26:08 +0100 Subject: [PATCH 06/15] Deliver the React Native Mobile key during the Android phase The React Native skill promised that provisioning writes the key file the JavaScript app reads, but only the iOS program writes it, and Android runs first: the Android program keeps the key in .iterable/.env, which the app never reads, so a bare RN or Expo app had no key until the iOS phase, or at all when iOS was out of scope. ios-provision import-from-android already does exactly this. Its state is the first one the iOS program reaches when an Android setup exists, before any Apple step or Xcode check, and it writes .iterable/rn/iterable.local.ts or .env.local at 0600 without printing the key. The skill now runs it in the Android phase, guards the Expo .gitignore first (the import skips .env.local silently when git would commit it), and checks the file without reading the key. Reusing the one writer avoids a second copy of the file format in the Android program. The iOS phase then gets carry_scope for the Step 0 answers, which ios.md and INTENDED-FLOW now describe; INTENDED-FLOW also stops claiming both platforms share one scope record, since Android records none. Co-Authored-By: Claude Opus 5.5 --- INTENDED-FLOW.md | 24 ++++++++++++-------- iterable-provision/SKILL.md | 13 ++++++----- iterable-provision/reference/ios.md | 5 ++++- iterable-react-native/SKILL.md | 34 ++++++++++++++++++++++------- 4 files changed, 52 insertions(+), 24 deletions(-) diff --git a/INTENDED-FLOW.md b/INTENDED-FLOW.md index afb6138..1f469b6 100644 --- a/INTENDED-FLOW.md +++ b/INTENDED-FLOW.md @@ -156,21 +156,27 @@ Missing proof metadata is not evidence that the SDK integration is missing. A React Native app runs the Android path and then the iOS path, one after the other, under the `iterable-react-native` skill: -1. Record the scope once — features, identity mode, region, JWT policy. Both - platforms use that one record. -2. **Android first.** The Android ladder provisions Firebase and Iterable, the - skill writes the integration, and `iterable-android-verify` proves a push. - No iOS step starts before that verdict is green. -3. **iOS second.** The iOS provider path offers to reuse both Iterable keys, - the region, No JWT, and the test email from the Android setup, carries over - the feature scope from step 1 (passed with `--features`, shown again for +1. Confirm the scope once, in the skill's Step 0 — features and identity mode. + The Android ladder records none of it. The iOS path receives those answers + as `--features` / `--identity`, marks their origin (`scope_source` / + `identity_source` = `react-native-step0`), and shows them again before + recording them. Region and JWT policy come from the Android setup. +2. **Android first.** The Android ladder provisions Firebase and Iterable, + `ios-provision import-from-android --confirm` copies the Mobile key into the + file the JavaScript app reads (it runs no Apple step), the skill writes the + integration, and `iterable-android-verify` proves a push. No iOS step starts + before that verdict is green. +3. **iOS second.** The iOS provider path already holds both Iterable keys, the + region, No JWT, and the test email from that import, carries over the + feature scope from step 1 (passed with `--features`, shown again for confirmation), and adds the Apple/APNs prerequisites. The skill writes the native push code (bare) or relies on the Expo plugin, and `iterable-ios-verify` proves a push on a Release Simulator build. 4. The app is integrated when the verifier has passed for every platform in scope. One green platform is not done. -The Mobile key reaches the app as a gitignored file the tool writes — bare +The Mobile key reaches the app as a gitignored file the tool writes during the +Android phase — bare React Native `.iterable/rn/iterable.local.ts`, Expo `EXPO_PUBLIC_ITERABLE_API_KEY` in `.env.local` — and the app loads it so that a checkout without the file still builds. diff --git a/iterable-provision/SKILL.md b/iterable-provision/SKILL.md index 442cc58..d175548 100644 --- a/iterable-provision/SKILL.md +++ b/iterable-provision/SKILL.md @@ -129,12 +129,13 @@ Inspect the app before invoking a program: - For an iOS target owned by a React Native or Expo integration, keep the React Native skill in charge of application code. Use the iOS provisioning path only for the Apple and Iterable prerequisites it explicitly needs. Run - the Android program first: the iOS path then offers to reuse its Mobile and - Server-side keys, region, No JWT policy, and test email, plus the feature - scope the React Native skill confirmed when passed with `--features`, and - writes the Mobile key where the JavaScript app reads it - (bare React Native `.iterable/rn/iterable.local.ts`, Expo - `EXPO_PUBLIC_ITERABLE_API_KEY` in a gitignored `.env.local`). + the Android program first. Its `import-from-android --confirm` reuses the + Android Mobile and Server-side keys, region, No JWT policy, and test email, + and writes the Mobile key where the JavaScript app reads it (bare React + Native `.iterable/rn/iterable.local.ts`, Expo `EXPO_PUBLIC_ITERABLE_API_KEY` + in a gitignored `.env.local`). It runs no Apple step, so the React Native + skill runs it during the Android phase; the feature scope that skill + confirmed follows in the iOS phase, passed with `--features`. Do not run the Android gate ladder against an iOS project. Its Firebase, `adb`, package-name, and FCM conclusions do not apply to APNs. diff --git a/iterable-provision/reference/ios.md b/iterable-provision/reference/ios.md index dad180d..dcbc577 100644 --- a/iterable-provision/reference/ios.md +++ b/iterable-provision/reference/ios.md @@ -122,7 +122,10 @@ recommended option passes the same `--features` and `--identity` to `import-from-android`, which records them as `carry-scope` would. A carried identity takes precedence over the Android test email. Without the flags the feature questions follow as usual. **Set up iOS separately** asks everything -again. In a monorepo the Android workspace sits +again. A React Native app usually ran `import-from-android --confirm` without +flags during its Android phase, to deliver the Mobile key to the JavaScript +app; `import_android` is then already answered, and the Step 0 flags passed +to `status` return the `carry_scope` screen instead. In a monorepo the Android workspace sits at the repository root, so the search walks up from the app root to the git toplevel, takes the nearest `.iterable/.env`, and records that path as `iterable.imported_from`. Android package auto-detection does not support diff --git a/iterable-react-native/SKILL.md b/iterable-react-native/SKILL.md index 9322e8c..2b0e28b 100644 --- a/iterable-react-native/SKILL.md +++ b/iterable-react-native/SKILL.md @@ -180,13 +180,17 @@ doc for a native step. When the scope covers both platforms, finish one before starting the other: -1. **Android:** provision with `iterable-provision`, integrate, and prove it — - `iterable-android-verify` green — before any iOS work starts. -2. **iOS:** `iterable-provision`'s iOS path offers to reuse what the Android - phase recorded — both Iterable keys (Mobile and Server-side), the region, - No JWT, and the test email. Pass the scope confirmed in Step 0 as - `--features ` from the table above, first to `ios-provision status`, - so the import screen names it, and its recommended option carries it into +1. **Android:** provision with `iterable-provision`, deliver the Mobile key to + the JavaScript app ([placing it](#if-they-dont-have-an-input-yet)), integrate, + and prove it — `iterable-android-verify` green — before any iOS work starts. + The key import runs no Apple step, so it is not iOS work. +2. **iOS:** `iterable-provision`'s iOS path already holds what that import + copied — both Iterable keys (Mobile and Server-side), the region, No JWT, + and the test email. Pass the scope confirmed in Step 0 as + `--features ` from the table above, first to `ios-provision status`: + it comes back as one `carry_scope` screen whose **Use these answers** + records it. If the import never ran, the `import_android` screen names the + scope instead and its recommended option carries it into `import-from-android`. The final scope screen shows it again before anything is confirmed. Pass only what the developer confirmed; without `--features` the scope is asked again. The path then adds the Apple/APNs @@ -247,7 +251,7 @@ everything". | Input | Needed when | If missing | |---|---|---| | **Workflow** — Expo vs bare | Always | Detect from the project (Step 0). If both signals exist, ask. | -| **Mobile API key** | Always | Never hardcode it into a tracked file. `iterable-provision` writes the copy the app reads: bare RN `.iterable/rn/iterable.local.ts`, Expo `EXPO_PUBLIC_ITERABLE_API_KEY` in a gitignored `.env.local` ([loading it](#loading-the-key)). | +| **Mobile API key** | Always | Never hardcode it into a tracked file. The Android program keeps it in `.iterable/.env`, which the app does not read; `ios-provision import-from-android --confirm` writes the copy the app reads — bare RN `.iterable/rn/iterable.local.ts`, Expo `EXPO_PUBLIC_ITERABLE_API_KEY` in a gitignored `.env.local` — during the Android phase ([placing it](#if-they-dont-have-an-input-yet), [loading it](#loading-the-key)). | | **Identity model** — `setEmail` vs `setUserId`, and where the value comes from | Always | [Project type first](#identity-project-type-first). Never guess. | | **JWT?** — is the mobile key JWT-protected? | Always | Ask, and **assume yes until told otherwise** (it is on by default for client-side keys and is permanent). If yes, `authHandler` is mandatory (rule 1) **and the team needs a backend endpoint that mints the tokens** — if they don't have one, raise it as a blocker instead of signing in the app. | | **Data region** — US or EU | Always | Ask if they sign in at `app.eu.iterable.com` (`european-data-center`). See pitfall #2. | @@ -295,6 +299,20 @@ placing the file**: on bare RN copy `.iterable/artifacts/google-services.json` to `android/app/google-services.json`; on Expo put it where `expo.android.googleServicesFile` points, never by hand-editing `android/`. Show it as part of your one confirmed diff. + +**The Mobile key, in the same step.** The Android program keeps it in +`.iterable/.env`, which the JavaScript app never reads. Deliver it with the iOS +program's import, which runs before any Apple step and never prints the key: from the +app root, run `/bin/ios-provision import-from-android --confirm`. It writes +`.iterable/rn/iterable.local.ts` (bare) or `EXPO_PUBLIC_ITERABLE_API_KEY` in +`.env.local` (Expo) at mode `0600`, and starts the self-ignoring `.iterable/ios/` +workspace the iOS phase reuses. On Expo it writes `.env.local` only when git already +ignores it, and does not say when it skipped — so run `git check-ignore -q .env.local` +first and, if that fails, add `.env*.local` to `.gitignore` in the confirmed diff +before the import. Check the result without reading the key: `test -f +.iterable/rn/iterable.local.ts`, or `grep -c '^EXPO_PUBLIC_ITERABLE_API_KEY=' +.env.local`. Leave the JSON's `next` alone: it is the iOS scope screen, which +belongs to the iOS phase. The import runs once per app. In a monorepo whose app sits below the repository root, provisioning does not read the Android package name from the project yet; the developer types it. From f3f1bb83beb0a9da50a5b748d73a63bc5b4ff516 Mon Sep 17 00:00:00 2001 From: Franco Zalamena Date: Thu, 22 Oct 2026 14:26:31 +0100 Subject: [PATCH 07/15] Label ios-send target as instrumented-only in the iOS reference The reference presented ios-send target as the way to send user-targeted proof, but standard proof sends through ios-prove --confirm, the one actor the verifier names in next.command. Agents following the reference on a standard run were reaching for the instrumented sender. Co-Authored-By: Claude Opus 5.5 --- iterable-provision/reference/ios.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/iterable-provision/reference/ios.md b/iterable-provision/reference/ios.md index dcbc577..3325a65 100644 --- a/iterable-provision/reference/ios.md +++ b/iterable-provision/reference/ios.md @@ -668,7 +668,15 @@ reviewing app code: The verifier cannot distinguish a repeated manual action from duplicate event tracking after both have reached Iterable. -Use the separate sender actor for user-targeted proof: +Standard proof sends through `ios-prove`, once, when the verifier's +`next.command` names it: + +```text +/bin/ios-prove --confirm +``` + +Instrumented runs only: the separate sender actor sends the user-targeted +marker: ```text /bin/ios-observe inspect --kind notifications --purpose pre-send From b41187ec4758acc3515f7e6c9cfb5d587b626aa6 Mon Sep 17 00:00:00 2001 From: Franco Zalamena Date: Thu, 22 Oct 2026 14:27:41 +0100 Subject: [PATCH 08/15] Resolve symlinks before walking up to bin/ Cursor installs can link a skill directory into ~/.cursor/skills. Walking up from the link's path never reaches the plugin's bin/, so the skill concluded the install was incomplete. Every skill that finds bin/ by walking up now says to start from the real path, in the same sentence. Co-Authored-By: Claude Opus 5.5 --- iterable-android-verify/SKILL.md | 5 ++++- iterable-android/SKILL.md | 4 +++- iterable-ios-verify/SKILL.md | 5 ++++- iterable-provision/SKILL.md | 4 +++- iterable-react-native/SKILL.md | 8 +++++--- 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/iterable-android-verify/SKILL.md b/iterable-android-verify/SKILL.md index 28d657e..4c25774 100644 --- a/iterable-android-verify/SKILL.md +++ b/iterable-android-verify/SKILL.md @@ -31,7 +31,10 @@ the human question, and wait. ## Finding the scripts -Start in the directory containing this `SKILL.md` and walk up to the first +Resolve symlinks first: use the real path of this `SKILL.md` (`cd -P` / +`pwd -P`), because a skill linked into `~/.cursor/skills` has no `bin/` above +the link. +Start in the directory containing it and walk up to the first directory containing a `bin/agent`; call that ``. If you reach the filesystem root without finding it, the install is incomplete—say so and stop. Never reimplement a gate by hand: a check you improvise is one nobody has diff --git a/iterable-android/SKILL.md b/iterable-android/SKILL.md index 02b07bd..a4fdb4b 100644 --- a/iterable-android/SKILL.md +++ b/iterable-android/SKILL.md @@ -304,7 +304,9 @@ it reads, what it will not do — before it runs anything else. Let it. Do not r deciding that provisioning is needed is not permission to begin it, and a project picker is not a consent screen. -To check it is installed: walk up from this `SKILL.md` to the first directory +Check it is installed. Resolve symlinks first: use the real path of this `SKILL.md` (`cd -P` / +`pwd -P`), because a skill linked into `~/.cursor/skills` has no `bin/` above +the link. Then walk up from it to the first directory containing a `bin/agent`. If there isn't one, this Preflight stops as written below, and the table of dashboard paths is what you give them. diff --git a/iterable-ios-verify/SKILL.md b/iterable-ios-verify/SKILL.md index 2c3c84a..4ed73ee 100644 --- a/iterable-ios-verify/SKILL.md +++ b/iterable-ios-verify/SKILL.md @@ -13,7 +13,10 @@ description: >- ## Finding the scripts -Start in the directory containing this `SKILL.md` and walk up to the first +Resolve symlinks first: use the real path of this `SKILL.md` (`cd -P` / +`pwd -P`), because a skill linked into `~/.cursor/skills` has no `bin/` above +the link. +Start in the directory containing it and walk up to the first directory containing `bin/ios-verify`; call that ``. If you reach the filesystem root without finding it, the install is incomplete—say so and stop. Never reimplement a gate by hand: a check you improvise is one nobody has diff --git a/iterable-provision/SKILL.md b/iterable-provision/SKILL.md index d175548..5f93aba 100644 --- a/iterable-provision/SKILL.md +++ b/iterable-provision/SKILL.md @@ -155,7 +155,9 @@ recollection of what you just did. ## Finding the scripts Hosts copy a plugin into a cache on install and set no environment variable, so no -path here is stable. Start in the directory containing this `SKILL.md` and walk up +path here is stable. Resolve symlinks first: use the real path of this `SKILL.md` (`cd -P` / +`pwd -P`), because a skill linked into `~/.cursor/skills` has no `bin/` above +the link. Start in the directory containing it and walk up until you reach the first directory containing a `bin/agent`. Call that ``. If you reach the filesystem root without finding it, the install is diff --git a/iterable-react-native/SKILL.md b/iterable-react-native/SKILL.md index 2b0e28b..ed57b4d 100644 --- a/iterable-react-native/SKILL.md +++ b/iterable-react-native/SKILL.md @@ -284,9 +284,11 @@ push integration themselves. It produces the three inputs this Preflight cannot invent — a real `google-services.json`, the service-account key that Iterable's push integration asks you to upload, and the dashboard steps that have no API — and proves each -one with a live call. Announce the handoff in one line and route. To check it -is installed, walk up from this `SKILL.md` to the first directory containing a -`bin/agent`; if there isn't one, this Preflight stops as written above. +one with a live call. Announce the handoff in one line and route. Check it is +installed. Resolve symlinks first: use the real path of this `SKILL.md` (`cd -P` / +`pwd -P`), because a skill linked into `~/.cursor/skills` has no `bin/` above +the link. Then walk up from it to the first directory containing a `bin/agent`; if there +isn't one, this Preflight stops as written above. **Routing is not starting.** The first thing that skill does is run `/bin/agent question opening` and put it to the developer before running anything From eede2f61043c96030e29401e4ff623f4ff89449b Mon Sep 17 00:00:00 2001 From: Franco Zalamena Date: Thu, 22 Oct 2026 14:32:36 +0100 Subject: [PATCH 09/15] Match the provisioning and verifier tables to what the programs route Several rows described routing the code does not do. The Iterable step screens carry the step in prompt (questions.sh builds it from iterable-keys --prompt) and use context only for the relay instruction, yet two passages told the agent the step was in context, which invites cutting prompt down to its question line. next_action reports investigate for a red token, registration, or delivery gate, and neither kind table had a row for it. ios-verify routes a standard physical-iPhone run through ios-prove with a read-back code; ios-deliver is reached only in instrumented runs. The opening-question section is also labelled Android, since the iOS path does not start there. Co-Authored-By: Claude Opus 5.5 --- iterable-android-verify/SKILL.md | 1 + iterable-ios-verify/SKILL.md | 13 +++++++------ iterable-provision/SKILL.md | 28 ++++++++++++++++------------ 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/iterable-android-verify/SKILL.md b/iterable-android-verify/SKILL.md index 4c25774..7ed4722 100644 --- a/iterable-android-verify/SKILL.md +++ b/iterable-android-verify/SKILL.md @@ -88,6 +88,7 @@ upstream is broken tests a system that is still wrong. | `install_app` | Their app is not on the device, or the installed APK does not carry the SDK. Build and install it, run `/bin/launch-app`, then re-run the agent. The check reads `dumpsys package` for the Iterable messaging service, so it catches "integrated the SDK, forgot to reinstall"—invisible to anything that only reads the repository. | | `run_app` | Run `/bin/launch-app` first, then relay `next.question`. It asks them to sign in as `target.email` and allow notifications, and one option re-runs the checks. Opening the launcher activity is yours; driving the UI or answering the permission dialog is never yours. | | `send_proof` | Announce it, then run `/bin/proof-push`. It sends one marked push and deliberately does not check whether it arrived. Then re-run `/bin/agent`. | +| `investigate` | A device-half check is red: no FCM token, the token not registered in Iterable, or a sent push that did not arrive. It has no command. Relay `next.summary` verbatim; it is the diagnosis of a defect, not pending work. Find the cause (the platform skill's `PITFALLS.md`) before changing anything, never re-send to get a pass, and re-run `/bin/agent` once it is fixed. | | `campaign_send` | Server-side corroboration needs `ITBL_CAMPAIGN_ID`; without it there is nothing to read. Optional. | | `iterable_keys`, `provision`, `choose_target`, … | Not this skill's half—hand back to `iterable-provision`. | | `done` | The push arrived. Say which push, on which device, and how long after the send. If `next.summary` also says the app has its own push path, relay that line and offer the optional after-integration check: the developer triggers one of the app's own notifications and confirms it shows. Run it only if they ask. It never changes the verdict. | diff --git a/iterable-ios-verify/SKILL.md b/iterable-ios-verify/SKILL.md index 4ed73ee..1a16779 100644 --- a/iterable-ios-verify/SKILL.md +++ b/iterable-ios-verify/SKILL.md @@ -160,10 +160,10 @@ When `next.kind` is: | `repair_device_connection` / `retry_device_read` | The device inventory was unreadable. Repair the connection as the summary says; `retry_device_read` re-runs the verifier once. | | `build_app` | Build first and record the resulting exact `.app` path. `ios-install` never builds. | | `install_app` | Run `ios-install` for the already-built exact `.app`, then re-run. | -| `prepare_delivery` / `prepare_delivery_b` | Physical iPhone or instrumented run only. Run the exact purpose-scoped SpringBoard inspection from `next.command`. | -| `deliver_user_a` / `deliver_user_b` | Physical iPhone or instrumented run only. Announce the exact identity and marker, then run the exact confirmed `ios-deliver target` command. | -| `observe_user_targeted` / `retry_delivery_observation` / `observe_user_b` | Physical iPhone or instrumented run only. Run the exact `ios-observe delivery` command without resending. | -| `inspect_delivery` / `inspect_delivery_logs` | Physical iPhone or instrumented run only. Run the exact diagnostic command; these never authorize another send. | +| `prepare_delivery` / `prepare_delivery_b` | Instrumented run only. Run the exact purpose-scoped SpringBoard inspection from `next.command`. | +| `deliver_user_a` / `deliver_user_b` | Instrumented run only. Announce the exact identity and marker, then run the exact confirmed `ios-deliver target` command. | +| `observe_user_targeted` / `retry_delivery_observation` / `observe_user_b` | Instrumented run only. Run the exact `ios-observe delivery` command without resending. | +| `inspect_delivery` / `inspect_delivery_logs` | Instrumented run only. Run the exact diagnostic command; these never authorize another send. | | `review_send_manifest` | Stop: the send manifest is invalid or incomplete. Do not send again until the run is reviewed or restarted. | | `launch_app` / `restore_app` | Instrumented run only. Run the exact `ios-launch` command, then re-run. | | `observe_tap` / `observe_logout` / `review_instrumentation` | Instrumented run only. The developer performs the recorded tap or logout; `review_instrumentation` reports missing journal evidence without repeating a UI action. | @@ -188,8 +188,9 @@ that receipt: it re-reads the same Simulator store and log itself, and after the developer clears Notification Center it accepts only the full log chain (received with this nonce, requested, saved, delivered). -On a physical iPhone and in instrumented runs, `ios-launch`, `ios-send`, and -`ios-observe` stay separate actors and `ios-deliver` sequences them: it waits +A standard run on a physical iPhone also sends through `ios-prove`, and the +developer reads the code back (`readback_code`). In instrumented runs, +`ios-launch`, `ios-send`, and `ios-observe` stay separate actors and `ios-deliver` sequences them: it waits for a secure observer arm receipt before invoking the sender once. `ios-observe inspect` non-interactively reads accessibility state and a bounded, redacted relevant log tail, then caches a mode-`0600`, short-lived diff --git a/iterable-provision/SKILL.md b/iterable-provision/SKILL.md index 5f93aba..967c3c3 100644 --- a/iterable-provision/SKILL.md +++ b/iterable-provision/SKILL.md @@ -164,9 +164,10 @@ If you reach the filesystem root without finding it, the install is incomplete. Say so and stop. Do not guess at a path, and do not reimplement any of these checks by hand. -## Before you run anything +## Android: before you run anything -The first command is not the ladder. It is the question that asks whether to start one: +This section is the Android program's; the iOS path starts as *Choose the platform before +running a ladder* says. On Android, the first command is not the ladder. It is the question that asks whether to start one: ``` /bin/agent question opening @@ -174,8 +175,8 @@ The first command is not the ladder. It is the question that asks whether to sta **Ask it, and wait for an answer, before you run any other command here.** It spends no credentials, reads nothing, and writes nothing — not even a workspace directory — so it -is safe to be the first thing that happens. Render it exactly as *When the tool hands you -a question* says: `context` above the selector, verbatim, then the options. +is safe to be the first thing that happens. Render it exactly as *Rendering a tool screen* says: +`context` printed above the selector, verbatim, then `prompt` whole and the options. This ordering is the point and it is easy to get wrong. A status read runs the whole eighteen-gate ladder against their Firebase projects, so a question carried in its output @@ -237,8 +238,9 @@ Render it as one `AskUserQuestion`: version of it still comes back "yes". - `requires`, when it is not empty, goes in the same message. It is what they have to do themselves, in their own terminal. -- `header` is the chip, `prompt` is the question, and each entry in `options` is one - option — `label` as the label, `description` as the description. +- `header` is the chip and `prompt` goes in whole, line breaks included: on a step screen + it carries the step itself, with the question as its last line. Each entry in + `options` is one option — `label` as the label, `description` as the description. - When they pick one, run that option's `commands` in order, then read the ladder again. - An option with a non-empty `relay` has **no** commands for you. That is a command only the developer can run — it hosts a sign-in, or takes a key with the echo off. Print it, @@ -347,6 +349,7 @@ relay the block, do not look for a flag that gets you past it, and never set | `iterable_keys` | Four steps in their Iterable account that only they can do. Ask `next.question`, then walk them **one at a time** — see *The four Iterable steps* below. The two keys reach the workspace with `iterable-keys --take server\|mobile`, off their clipboard; never into this conversation. | | `install_app` / `run_app` | The Google and Iterable halves are green. If the app has no Iterable integration yet, continue as *Where this goes next* says first. Otherwise this is the Android device half—hand off to `iterable-android-verify`. Until a device is on record `next.question` is the device picker, worth asking even when only one is attached: a phone left plugged in to charge is "the only device", takes the proof push, and the developer watching an emulator reports that nothing arrived. The verifier may open the selected app after installation; signing in and allowing notifications still happen inside their app, by their hand. | | `send_proof` | The Android device half—hand off to `iterable-android-verify`. | +| `investigate` | A device-half check is red (token, registration, or delivery). Relay `next.summary` verbatim—it is a defect, not pending work—and hand off to `iterable-android-verify`, which owns it. | | `done` | A real push reached the device. Say which device and chain onward. | `next.command` is the command for that step when there is one. Run it; do not @@ -471,15 +474,16 @@ between is to keep it to **one step per message**. these four cannot be automated, and that the keys never come through this conversation. Ask it. Then, for each step the chain gives you: -1. Run `/bin/agent question iterable_step N`. Its `context` **is** the step — the - numbered clicks, the URL, and the one or two arrows that matter — so show every line of - it above the selector, as it came out. Do not summarise it, do not retype the URLs, do +1. Run `/bin/agent question iterable_step N`. Its `prompt` **is** the step — the + numbered clicks, the URL, and the one or two arrows that matter — with the question as + its last line, so pass it to `AskUserQuestion` whole, as it came out; its `context` + is the relay instruction, printed above the selector as usual. Do not summarise it, do not retype the URLs, do not merge two steps into one message. A shortened step is the most expensive mistake in this whole setup: "FCM type: Data notifications" condensed to "enable notifications" breaks push while every other check still reads as configured. 2. Then wait. Step N+1 does not exist until they answer, and the answer names it. If you - find yourself writing the instructions for a dashboard screen, you have skipped the - `context` — nothing here needs you to compose a step, and a composed one has already + find yourself writing the instructions for a dashboard screen, you have cut the + `prompt` — nothing here needs you to compose a step, and a composed one has already told a developer to rename a file this tool stopped writing. 3. Its options arrive with the commands that enact them, so the walk cannot lose its place or skip a screen. On step 1 two of them are `--take server` and `--take mobile`: @@ -514,7 +518,7 @@ writes `.iterable/.env` at mode 0600 with three empty names — never overwritin value, because an Iterable key is shown once. Without arguments and without a terminal it prints all four at once — that is the form for a script, not for a conversation. You do not need to call it on the chat path: `agent question iterable_step N` already -carries the same clicks in its `context`, from the same source. +carries the same clicks in its `prompt`, from the same source. - **Never ask anyone to paste a key into the conversation.** Not as a question, not as an `AskUserQuestion` option, not "just to check the format". `--take` exists so From 1cf83408e5424f9eabcb791a0e4a124315df83a3 Mon Sep 17 00:00:00 2001 From: Franco Zalamena Date: Thu, 22 Oct 2026 14:32:36 +0100 Subject: [PATCH 10/15] Resolve contradictions between the platform skills and the flow doc - The identity relaunch is the identity screen's opt-in read-identity option, not something the verifier does; INTENDED-FLOW's "never relaunches" now says the same. - React Native binds the Release build through the App build screen, or restart --confirm --app-path for an existing run, instead of telling the agent to append --app-path by hand. - The React Native pitfall index and its Expo-only note now agree with the headings and with #3, where both workflows request at runtime. - The native iOS "ask in context" rule notes that bare React Native asks at launch in AppDelegate, which is the intended RN behaviour. - "Not in this skill" sends dashboard questions to the shipped doc first, as Preflight already does. - Xcode registers the App ID only for a device build's profile, so a Simulator-only app usually lacks it; the Android proof runs on a device or an emulator. Co-Authored-By: Claude Opus 5.5 --- INTENDED-FLOW.md | 5 ++++- iterable-android/SKILL.md | 8 +++++--- iterable-ios/SKILL.md | 19 +++++++++++++------ iterable-react-native/PITFALLS.md | 18 +++++++++++------- iterable-react-native/SKILL.md | 17 +++++++++++------ 5 files changed, 44 insertions(+), 23 deletions(-) diff --git a/INTENDED-FLOW.md b/INTENDED-FLOW.md index 1f469b6..e70a278 100644 --- a/INTENDED-FLOW.md +++ b/INTENDED-FLOW.md @@ -108,7 +108,10 @@ integration or Apple provisioning flow: 3. A new integration completed in the current session may record its exact built artifact; a standalone verification reuses the installed application and reports that the current source/build is not linked to that proof. - `ios-install` never builds. Standard proof never relaunches the app. + `ios-install` never builds. Standard proof never relaunches the app on its + own; the identity screen's **Relaunch the app to read the identity it signs + in with** option (`ios-prepare-proof read-identity`) does, and only when the + developer picks it. 4. The verifier checks the exact user and device registration remotely. On a Simulator it also checks that Iterable holds that Simulator's current APNs token. Before requesting one missing manual action it performs short-lived, diff --git a/iterable-android/SKILL.md b/iterable-android/SKILL.md index a4fdb4b..9be5038 100644 --- a/iterable-android/SKILL.md +++ b/iterable-android/SKILL.md @@ -186,7 +186,7 @@ If `iterable-provision` is installed, its `bin/agent` carries this as a `notice` field — relay that verbatim instead, so the tool and the conversation say the same thing. Either way say it once, plainly, and do not soften it into "I've double-checked everything". Proof is the next section, and the strongest form of it is a real push -landing on a real device—`iterable-android-verify` does that. +landing on a device or emulator—`iterable-android-verify` does that. ### Proving it works when you don't have the dashboard @@ -813,8 +813,10 @@ before writing any code is the fastest way to compact mid-task. native Android app. - Native iOS — that is the sibling skill `iterable-ios`. The Web SDK is not covered by these skills. -- Iterable platform / dashboard configuration. Direct the user to - [support.iterable.com](https://support.iterable.com) for dashboard help. +- Iterable platform / dashboard configuration. Open the `reference/` doc that + covers the step (*Slug routing*), as Preflight says; point to + [support.iterable.com](https://support.iterable.com) only when no shipped doc + covers it. - JWT *server-side* implementation. The skill assumes the team has, or will build, a token-minting endpoint. The `identifying-the-user` doc covers the client side only. diff --git a/iterable-ios/SKILL.md b/iterable-ios/SKILL.md index dfa8a02..ec3dd29 100644 --- a/iterable-ios/SKILL.md +++ b/iterable-ios/SKILL.md @@ -190,8 +190,10 @@ question, and run only the command carried by the option they pick. Do not background or delegate the whole flow, don't edit the app in parallel while it waits on Apple, Iterable, or the developer, and never use the Android/Firebase provisioner (`gcp-provisioner`) for iOS. On the Apple side the developer -registers the Bundle ID with Push Notifications in the Apple Developer portal, -or Xcode's automatic signing does it. +registers the Bundle ID with Push Notifications in the Apple Developer portal. +Xcode's automatic signing registers it when it makes a provisioning profile +for a device build; a Simulator-only build does not make Xcode register it, so +for a new app it is usually missing. **Never fabricate a prerequisite to make the build pass.** No fake or empty key presented as working, no server-side key in the app, no identity wired to a @@ -697,6 +699,9 @@ func requestNotificationPermission() async { `.denied` means stop asking; offer the app's page in system Settings instead. `.authorized`, `.provisional`, and `.ephemeral` mean don't prompt again. +That timing is for native apps. A bare React Native app asks at launch in +`AppDelegate`, as `iterable-react-native` says; keep it there. + --- ## Project setup (plain `.xcodeproj`) @@ -1002,9 +1007,11 @@ to act again.** Its last answer was about the build you replaced. When push is in scope, add this file and call `IterableProofMarker.log()` on the line after `IterableAPI.initialize(...)` in `start` and after the -`setEmail` / `setUserId` call in `identify`. At proof time the verifier -relaunches the app with `-iterableProofIdentity YES`, reads the identity from -the Simulator's log, and offers it once Iterable holds this Simulator's device +`setEmail` / `setUserId` call in `identify`. At proof time the identity +screen's **Relaunch the app to read the identity it signs in with** option +(`ios-prepare-proof read-identity --confirm`) relaunches the app with +`-iterableProofIdentity YES`, reads the identity from the Simulator's log, and +offers it once Iterable holds this Simulator's device token on that user, so nobody has to dig out a generated user ID. A normal launch prints nothing; the launch argument, not `#if DEBUG`, gates it, because a Release build can be the one proved. @@ -1029,7 +1036,7 @@ as the last step of the handoff; `ios-verify inspect` and the verifier's done summary warn `proof_identity_marker_present` until it is gone. If you already know the signed-in identity from device evidence (the app's log, a debugger read), pass it as `/bin/ios-prepare-proof status --identity-candidate -`; the tool checks it against Iterable and offers it. Never type an +` (`` as `iterable-ios-verify` finds it); the tool checks it against Iterable and offers it. Never type an identity into the proof screen on the developer's behalf. An iOS 16+ Simulator on macOS 13+ (Apple silicon or T2 Mac) receives real APNs diff --git a/iterable-react-native/PITFALLS.md b/iterable-react-native/PITFALLS.md index 24db45c..d666f0d 100644 --- a/iterable-react-native/PITFALLS.md +++ b/iterable-react-native/PITFALLS.md @@ -24,9 +24,10 @@ config-plugin traps (build-time and configuration-time). Use them only after Step 0 classified the project as Expo. **#12** is Xcode **27** on iOS (bare **and** Expo): Device Hub launch and resource-bundle compile floors — not an Iterable API change. **#15** is native Android and applies -to **both** workflows, but only when the app already sends push. #3's -*symptom* is both workflows; its **bare** native fix (manifest + -`PermissionsAndroid`) is wrong on Expo — branch first. **#16** applies to every +to **both** workflows, but only when the app already sends push. #3 is +both workflows, and both request the permission at runtime with +`PermissionsAndroid`; only its **bare** manifest edit is wrong on Expo, where +the plugin declares it — branch first. **#16** applies to every app with deep links; **#17** is Expo only. **#18** is the install, both workflows. **#19–#20** are the iOS Release Simulator build (bare, and Expo when built with `xcodebuild`); **#21** is JavaScript, both workflows; **#22** is bare @@ -194,8 +195,9 @@ iOS (on Expo, #14). --- -Pitfalls 7–13 apply only when Step 0 detected Expo. Skip them on a bare -React Native project. Do not introduce `@iterable/expo-plugin` there. +Pitfalls 7–11, 13, 14 and 17 apply only when Step 0 detected Expo; #12 is +Xcode 27 on both workflows. Skip the Expo ones on a bare React Native +project. Do not introduce `@iterable/expo-plugin` there. ## 7. Expo: SDK and plugin do not work in Expo Go @@ -501,8 +503,10 @@ Pitfall 15 applies to **both** workflows — any app that already sends push. (say, from an old `ios/build`) is whatever was built there last. - **Fix:** Build with the exact command in `SKILL.md` → Bare iOS (`-derivedDataPath build`), install - `ios/build/Build/Products/Release-iphonesimulator/.app`, and pass that - path to `ios-prepare-proof start --app-path`. + `ios/build/Build/Products/Release-iphonesimulator/.app`, and bind it + through the verifier's **App build** screen, or with + `ios-prepare-proof restart --confirm --app-path ` when a run already + exists. ## 20. iOS: code signing disabled strips `aps-environment` diff --git a/iterable-react-native/SKILL.md b/iterable-react-native/SKILL.md index ed57b4d..6f1bc3b 100644 --- a/iterable-react-native/SKILL.md +++ b/iterable-react-native/SKILL.md @@ -724,9 +724,11 @@ and `[IterableAppIntegration userNotificationCenter:didReceiveNotificationRespon ``` The app is `ios/build/Build/Products/Release-iphonesimulator/.app`. - Install that path with `xcrun simctl install `, and record it - with `/bin/ios-prepare-proof start --confirm --app-path `, or - `restart --confirm --app-path ` when a run already exists. Never install a `.app` found with `find` or from another DerivedData + Install that path with `xcrun simctl install `. The verifier's + **App build** screen then finds it under the app's own `-derivedDataPath` and + recommends binding it; run the option the developer picks, and never append + `--app-path` by hand. When a run already exists, bind the rebuilt `.app` with + `/bin/ios-prepare-proof restart --confirm --app-path `. Never install a `.app` found with `find` or from another DerivedData folder — it can be a stale build (pitfall #19). Never add `CODE_SIGNING_ALLOWED=NO`, `CODE_SIGNING_REQUIRED=NO` or `CODE_SIGN_IDENTITY=""`: an unsigned app carries no `aps-environment` and never gets a token @@ -734,7 +736,8 @@ and `[IterableAppIntegration userNotificationCenter:didReceiveNotificationRespon - **Proof identity marker (temporary):** when push is in scope, add this to `AppDelegate.swift` and call it as the last line of `application(_:didFinishLaunchingWithOptions:)`, before `return true`. At - proof time the verifier relaunches the app with `-iterableProofIdentity YES` + proof time the identity screen's relaunch option (`ios-prepare-proof + read-identity --confirm`) relaunches the app with `-iterableProofIdentity YES` and reads the identity from the Simulator's log, as in `iterable-ios` (*The proof identity marker*). It reads the native SDK, not JavaScript: the identity is stored natively once `Iterable.initialize` and `setEmail` / @@ -903,8 +906,10 @@ you detected), then load feature slugs **as you reach each feature**. - Native Android-only or iOS-only app integrations — those are `iterable-android` and `iterable-ios`. If this project is a native app with no React Native, stop and use the matching native skill. -- Iterable platform / dashboard configuration. Direct the user to - [support.iterable.com](https://support.iterable.com). +- Iterable platform / dashboard configuration. Open the `reference/` doc that + covers the step (*Slug routing*), as Preflight says; point to + [support.iterable.com](https://support.iterable.com) only when no shipped doc + covers it. - JWT *server-side* implementation. Assume the team has, or will build, a token-minting endpoint. From f3ca39f2d4aba2a1c0e8ffd0f7c5dfab34621010 Mon Sep 17 00:00:00 2001 From: Franco Zalamena Date: Thu, 22 Oct 2026 14:53:15 +0100 Subject: [PATCH 11/15] Add a local-only bin/agent preflight that names the next step An integration skill needs to know, before asking or editing anything, whether the prerequisites exist. preflight reads only local files (the google-services.json copies, key presence in .iterable/.env, the React Native key file, the last ladder record), writes nothing, prints no key value, and flags placeholder or .example files as missing. Its next is provision, integrate, verify or done, with the file to open and the exact commands to run, so a weak model has no order to infer. Co-Authored-By: Claude Opus 5.5 --- bin/agent | 83 ++++++++++++++++++++++++++++++++++++ tests/agent-preflight.sh | 90 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100755 tests/agent-preflight.sh diff --git a/bin/agent b/bin/agent index 31c8bf3..a30afc4 100755 --- a/bin/agent +++ b/bin/agent @@ -9,6 +9,7 @@ # bin/agent approve firebase record the developer's yes to changing their project # bin/agent set DRIVER=agent they asked you to run the setup instead of the wizard # bin/agent question K [N] one question to put to the developer, as data +# bin/agent preflight local files only: what exists, and provision/integrate/verify # # `next.question` is how the wizard's screens reach a chat: the context to show, and the # two to four answers with the commands that enact each one. The words are the tool's @@ -57,6 +58,88 @@ if [[ "${1:-}" == question ]]; then exit 0 fi +# Before ws_init for the same reason as `question`: an integration skill runs this before +# anybody has agreed to anything, so it reads local files only and writes none. +if [[ "${1:-}" == preflight ]]; then + source "$BIN/ios-project.sh" + APP="$(dirname "$WS")" + FW="$(ios_project_framework "$APP")"; [[ "$FW" == native ]] && FW=android + jarr() { local s="" x; printf '['; for x in "$@"; do printf '%s%s' "$s" "$(jstr "$x")"; s=","; done; printf ']'; } + fake() { shopt -s nocasematch; [[ -z "$1" || "$1" =~ (your[_-]|replace[_-]?(me|with)|placeholder|dummy|changeme|example|xxxx|[<]) ]]; local r=$?; shopt -u nocasematch; return $r; } + declare -a MISS=() FAKES=() GS_FOUND=() DO=() + for f in "$GS_JSON" "$APP/android/app/google-services.json" "$APP/app/google-services.json" "$APP/google-services.json"; do + [[ -f "$f" ]] || continue + if grep -Eiq 'your[_-]|placeholder|dummy|changeme' "$f"; then FAKES+=("$f"); else GS_FOUND+=("$f"); fi + done + GS_IN_APP=false + for f in "${GS_FOUND[@]+"${GS_FOUND[@]}"}"; do [[ "$f" == "$GS_JSON" ]] || GS_IN_APP=true; done + ((${#GS_FOUND[@]})) || MISS+=("google-services.json") + MOBILE=false; SERVER=false + if fake "$ITBL_MOBILE_KEY"; then [[ -z "$ITBL_MOBILE_KEY" ]] || FAKES+=("$ITBL_ENV (ITBL_MOBILE_KEY)"); MISS+=("Iterable Mobile key"); else MOBILE=true; fi + if fake "$ITBL_SERVER_KEY"; then [[ -z "$ITBL_SERVER_KEY" ]] || FAKES+=("$ITBL_ENV (ITBL_SERVER_KEY)"); MISS+=("Iterable Server-side key"); else SERVER=true; fi + APP_KEY=null APP_KEY_PATH="" + case "$FW" in + react-native) APP_KEY_PATH="$APP/.iterable/rn/iterable.local.ts" + v="$(sed -nE 's/.*ITERABLE_API_KEY[^"]*"([^"]*)".*/\1/p' "$APP_KEY_PATH" 2>/dev/null | head -1)" ;; + expo) APP_KEY_PATH="$APP/.env.local" + v="$(sed -n 's/^EXPO_PUBLIC_ITERABLE_API_KEY=//p' "$APP_KEY_PATH" 2>/dev/null | head -1)" ;; + esac + if [[ -n "$APP_KEY_PATH" ]]; then + if fake "$v"; then APP_KEY=false; [[ -z "$v" ]] || FAKES+=("$APP_KEY_PATH"); else APP_KEY=true; fi + fi + for f in "$APP/.iterable/rn/iterable.local.ts.example" "$APP/.env.local.example" \ + "$APP/android/app/google-services.json.example" "$APP/app/google-services.json.example"; do + [[ -f "$f" ]] && FAKES+=("$f") + done + OPEN_GATE=""; PROVEN=false; PUSH=false + if [[ -f "$WS/state.tsv" ]]; then + OPEN_GATE="$(awk -F'\t' '$2=="red"||$2=="pending"{print $1; exit}' "$WS/state.tsv")" + [[ "$OPEN_GATE" =~ ^G([0-9]|1[0-2])$ ]] || PROVEN=true + push_proven && PUSH=true + fi + SDK=false + if [[ "$FW" == android ]]; then + grep -qs 'com.iterable:iterableapi\|iterableapi' "$APP"/app/build.gradle* "$APP"/gradle/libs.versions.toml && SDK=true + else + grep -qs '"@iterable/react-native-sdk"' "$APP/package.json" && SDK=true + fi + PROVISION="$TOOL_ROOT/iterable-provision/SKILL.md" + if ((${#MISS[@]})) || [[ "$PROVEN" != true ]]; then + KIND=provision OPEN="$PROVISION"; DO=("$BIN/agent question opening") + LIST="$(IFS=,; printf '%s' "${MISS[*]+"${MISS[*]}"}")" + SUMMARY="Prerequisites are missing or not yet proven${LIST:+ (missing: ${LIST//,/, })}. Write no code and no placeholder. Open $PROVISION now, follow it, and run its first command: $BIN/agent question opening" + elif [[ "$GS_IN_APP" != true || "$APP_KEY" == false || "$SDK" != true ]]; then + KIND=integrate OPEN="" + if [[ "$GS_IN_APP" != true ]]; then + case "$FW" in + react-native) DO+=("cp '$GS_JSON' android/app/google-services.json") ;; + android) DO+=("cp '$GS_JSON' app/google-services.json") ;; + esac + fi + if [[ "$APP_KEY" == false ]]; then + [[ "$FW" == expo ]] && ! git -C "$APP" check-ignore -q .env.local 2>/dev/null && DO+=("printf '.env*.local\\n' >> .gitignore") + DO+=("$BIN/ios-provision import-from-android --confirm") + fi + SUMMARY="Prerequisites are proven. Run these commands from the app root, then write the Android integration from the skill you came from." + [[ "$FW" == expo ]] && SUMMARY="$SUMMARY Point expo.android.googleServicesFile at a copy of $GS_JSON." + elif [[ "$PUSH" != true ]]; then + KIND=verify OPEN="$TOOL_ROOT/iterable-android-verify/SKILL.md"; DO=("$BIN/agent") + SUMMARY="Integration files are in place. Build and install the app, then open $OPEN and follow it." + else + KIND=done OPEN="" + SUMMARY="A push reached the Android device." + [[ "$FW" == android ]] || { DO=("$BIN/ios-provision status"); SUMMARY="$SUMMARY Android is finished. If iOS is in scope, start it with the command below and the --features from Step 0."; } + fi + printf '{"tool":"iterable-onboard","command":"preflight","local_only":true,"app_root":%s,"framework":%s,' "$(jstr "$APP")" "$(jstr "$FW")" + printf '"present":{"google_services_json":%s,"google_services_json_in_app":%s,"mobile_key":%s,"server_key":%s,"app_key_file":%s,"provisioning_proven":%s,"sdk_dependency":%s,"push_proven":%s},' \ + "$( ((${#GS_FOUND[@]})) && echo true || echo false)" "$GS_IN_APP" "$MOBILE" "$SERVER" "$APP_KEY" "$PROVEN" "$SDK" "$PUSH" + printf '"app_key_file":%s,"missing":%s,"placeholders":%s,' "$(jstr "$APP_KEY_PATH")" \ + "$(jarr "${MISS[@]+"${MISS[@]}"}")" "$(jarr "${FAKES[@]+"${FAKES[@]}"}")" + printf '"next":{"kind":%s,"open":%s,"commands":%s,"summary":%s}}\n' "$(jstr "$KIND")" \ + "$([[ -n "$OPEN" ]] && jstr "$OPEN" || echo null)" "$(jarr "${DO[@]+"${DO[@]}"}")" "$(jstr "$SUMMARY")" + exit 0 +fi + # So the shortcuts a developer gets handed exist from the first call, not from # whichever later one happened to run the verifier. ws_init diff --git a/tests/agent-preflight.sh b/tests/agent-preflight.sh new file mode 100755 index 0000000..bd9d7bd --- /dev/null +++ b/tests/agent-preflight.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# `bin/agent preflight` runs before any consent, so it must read local files only, +# write nothing, never print a key, and route a fresh app to provisioning. +# +# No network: gcloud and curl are stubs that record any call. + +set -uo pipefail +cd "$(dirname "$0")/.." +ROOT="$PWD" + +FAILED=0 +ok() { printf ' \033[32mPASS\033[0m %s\n' "$1"; } +bad() { printf ' \033[31mFAIL\033[0m %s\n' "$1"; FAILED=1; } + +TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT +STUB="$TMP/stub"; mkdir -p "$STUB" +for c in gcloud curl adb firebase; do + printf '#!/bin/sh\necho "%s $*" >> "%s/calls"\nexit 1\n' "$c" "$TMP" > "$STUB/$c"; chmod +x "$STUB/$c" +done +export PATH="$STUB:$PATH" + +MOBILE=0123456789abcdef0123456789abcdef +SERVER=fedcba9876543210fedcba9876543210 + +new_app() { # — a bare React Native app + rm -rf "$1"; mkdir -p "$1/android/app" + ( cd "$1" && git init -q ) + printf '{"dependencies":{"react-native":"0.76.0"}}\n' > "$1/package.json" +} +pf() { ( cd "$1" && "$ROOT/bin/agent" preflight 2>/dev/null ); } +field() { node -e 'let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{const j=JSON.parse(d);process.stdout.write(String(process.argv[1].split(".").reduce((o,k)=>o?.[k],j)))})' "$1"; } + +echo +echo " bin/agent preflight — local files only, before any consent" +echo + +A="$TMP/fresh"; new_app "$A" +OUT="$(pf "$A")" +[[ "$(field next.kind <<< "$OUT")" == provision ]] \ + && ok "nothing present routes to provision" || bad "fresh app got: $OUT" +[[ "$(field next.commands.0 <<< "$OUT")" == "$ROOT/bin/agent question opening" \ + && "$(field next.open <<< "$OUT")" == "$ROOT/iterable-provision/SKILL.md" ]] \ + && ok "and names the provisioning skill and its first command" || bad "next lacks the opening command: $OUT" +[[ ! -e "$A/.iterable" ]] && ok "and writes no workspace" || bad "preflight created $A/.iterable" + +B="$TMP/ready"; new_app "$B" +mkdir -p "$B/.iterable/artifacts" +printf '{"project_info":{"project_id":"real-project"}}\n' > "$B/.iterable/artifacts/google-services.json" +printf 'ITBL_MOBILE_KEY=%s\nITBL_SERVER_KEY=%s\n' "$MOBILE" "$SERVER" > "$B/.iterable/.env" +printf 'G10\tgreen\thuman\tIterable API keys work\t\nG12\tunverifiable\thuman\tPush integration configured\t\nG13\tpending\thuman\tApp installed with the SDK\t\n' > "$B/.iterable/state.tsv" +OUT="$(pf "$B")" +[[ "$(field next.kind <<< "$OUT")" == integrate ]] \ + && ok "proven prerequisites route to integrate" || bad "ready app got: $OUT" +grep -qF "import-from-android --confirm" <<< "$OUT" && grep -qF "android/app/google-services.json" <<< "$OUT" \ + && ok "and say how the key file and google-services.json reach the app" || bad "integrate lacks the delivery commands: $OUT" +grep -qF -e "$MOBILE" -e "$SERVER" <<< "$OUT" \ + && bad "a key value appears in the output" || ok "no key value in the output" + +cp "$B/.iterable/artifacts/google-services.json" "$B/android/app/" +mkdir -p "$B/.iterable/rn" +printf 'export const ITERABLE_API_KEY = "%s";\n' "$MOBILE" > "$B/.iterable/rn/iterable.local.ts" +node -e 'const f=process.argv[1],p=require(f);p.dependencies["@iterable/react-native-sdk"]="2.0.0";require("fs").writeFileSync(f,JSON.stringify(p))' "$B/package.json" +OUT="$(pf "$B")" +[[ "$(field next.kind <<< "$OUT")" == verify ]] \ + && ok "everything placed routes to verify" || bad "integrated app got: $OUT" +grep -qF "$MOBILE" <<< "$OUT" && bad "the delivered key appears in the output" || ok "and still prints no key" + +printf 'export const ITERABLE_API_KEY = "YOUR_API_KEY";\n' > "$B/.iterable/rn/iterable.local.ts" +OUT="$(pf "$B")" +[[ "$(field present.app_key_file <<< "$OUT")" == false && "$(field next.kind <<< "$OUT")" == integrate ]] \ + && grep -qF "iterable.local.ts" <<< "$(field placeholders <<< "$OUT")" \ + && ok "a placeholder key file is flagged and treated as missing" || bad "placeholder not flagged: $OUT" + +rm "$B/.iterable/rn/iterable.local.ts" +printf 'export const ITERABLE_API_KEY = "";\n' > "$B/.iterable/rn/iterable.local.ts.example" +OUT="$(pf "$B")" +grep -qF "iterable.local.ts.example" <<< "$(field placeholders <<< "$OUT")" \ + && ok "an .example key file is flagged" || bad ".example not flagged: $OUT" + +printf 'ITBL_MOBILE_KEY=YOUR_MOBILE_KEY\nITBL_SERVER_KEY=%s\n' "$SERVER" > "$B/.iterable/.env" +OUT="$(pf "$B")" +[[ "$(field next.kind <<< "$OUT")" == provision && "$(field present.mobile_key <<< "$OUT")" == false ]] \ + && ok "a placeholder Mobile key routes back to provision" || bad "placeholder Mobile key accepted: $OUT" + +[[ ! -s "$TMP/calls" ]] && ok "no gcloud, curl, adb or firebase call was made" \ + || bad "network tools were called: $(tr '\n' ';' < "$TMP/calls")" + +echo +((FAILED)) && { echo " FAILED"; exit 1; } +echo " All good — preflight is local, silent about secrets, and routes in order." From 6da9963ac6212ff7ee352baebe1496cf0e6bac88 Mon Sep 17 00:00:00 2001 From: Franco Zalamena Date: Thu, 22 Oct 2026 14:53:16 +0100 Subject: [PATCH 12/15] Open each integration skill with a mechanical Start here MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Cursor trial on a cost-optimized model skimmed the React Native skill, never ran bin/agent, never opened iterable-provision, wrote both platforms at once and a placeholder key file. Each integration skill now starts with a few numbered commands: find , run the local preflight (ios-provision status on iOS), open iterable-provision by path when prerequisites are missing, never write a placeholder, and on React Native finish Android before any iOS edit. Step 0 asks whether the prerequisites exist, recommending "Not yet — set them up". Co-Authored-By: Claude Opus 5.5 --- iterable-android/SKILL.md | 22 ++++++++++++++++++++++ iterable-ios/SKILL.md | 22 ++++++++++++++++++++++ iterable-react-native/SKILL.md | 23 +++++++++++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/iterable-android/SKILL.md b/iterable-android/SKILL.md index 9be5038..292f2dd 100644 --- a/iterable-android/SKILL.md +++ b/iterable-android/SKILL.md @@ -19,6 +19,16 @@ description: >- # Iterable Android SDK +## Start here — do these in order, before anything else + +1. Find ``: from the real path of this `SKILL.md` (`cd -P "$(dirname )" && pwd -P`), walk up to the first folder that contains `bin/agent`. +2. From the app root, run `/bin/agent preflight` before you ask anything or edit anything. It reads local files only. +3. Do what its `next` says: open `next.open` if it is set, then run `next.commands` in order. + - `provision`: open `/iterable-provision/SKILL.md` now and follow it. Its first command is `/bin/agent question opening`. Write no code until it hands back. + - `integrate`: run `next.commands`, then do Step 0 and write the integration. + - `verify`: open `/iterable-android-verify/SKILL.md` and follow it. +4. Never write a placeholder, dummy, `.example` or hard-coded key, or a fake `google-services.json`. A missing input comes from provisioning, or you stop and say so. + You are working with the **Iterable Android SDK** — [`com.iterable:iterableapi`](https://central.sonatype.com/artifact/com.iterable/iterableapi) (core) and `com.iterable:iterableapi-ui` (UI components for in-app, inbox, @@ -35,6 +45,18 @@ human readers but omit several silent-failure traps documented in ## Step 0 — Agree on scope BEFORE writing code +**Prerequisites question — ask it in the first round, with the scope.** "Do you +already have the Iterable Mobile key, the Iterable mobile app with its push +integration, and a Firebase project with this app's `google-services.json`?" + +- **Not yet — set them up** (recommended): open `/iterable-provision/SKILL.md` + now and run `/bin/agent question opening`. +- **Yes, I have them**: still open `iterable-provision`; it finds, reuses and + proves what exists. Never ask for a key in the chat. + +When `/bin/agent preflight` already returned `integrate` or `verify`, the +files exist: say so and only confirm. + Do this **first**, before Preflight and before any edits. The SDK covers many features — push, in-app, mobile inbox, embedded, deep links, event tracking, user profiles. Do **not** assume the developer wants all of them, and do **not** diff --git a/iterable-ios/SKILL.md b/iterable-ios/SKILL.md index ec3dd29..937f196 100644 --- a/iterable-ios/SKILL.md +++ b/iterable-ios/SKILL.md @@ -20,6 +20,16 @@ description: >- # Iterable iOS SDK +## Start here — do these in order, before anything else + +1. Find ``: from the real path of this `SKILL.md` (`cd -P "$(dirname )" && pwd -P`), walk up to the first folder that contains `bin/ios-provision`. +2. From the app root, run `/bin/ios-provision status` before you edit anything. It reads local files only and makes no Apple or Iterable request. +3. Act on its `next.kind`: + - `integrate_app`: do Step 0, then write the integration. + - `verify_app`: open `/iterable-ios-verify/SKILL.md` and follow it. + - anything else: prerequisites are missing. Ask Step 0's questions, then open `/iterable-provision/SKILL.md` and follow its iOS path, starting with `/bin/ios-provision status --features --identity `. Put each `next.question` to the developer and run only the command of the option they pick. Write no code until it reaches `integrate_app`. +4. Never write a placeholder, dummy, `.example` or hard-coded key in place of a real one. A missing input comes from provisioning, or you stop and say so. + You are working with **Iterable's Swift SDK**, [`Iterable/iterable-swift-sdk`](https://github.com/Iterable/iterable-swift-sdk): `IterableSDK` (the app target) and `IterableAppExtensions` (the Notification @@ -64,6 +74,18 @@ about facts you could read: provider stops receiving or showing pushes. Don't delete the provider's code unless they confirm that diff. +**Prerequisites question — ask it in the first round, with the scope.** "Do you +already have the Iterable Mobile key, the Iterable iOS app with its APNs push +credential, and an APNs `.p8` key for this Bundle ID?" + +- **Not yet — set them up** (recommended): open `/iterable-provision/SKILL.md` + now and follow its iOS path from `/bin/ios-provision status`. +- **Yes, I have them**: still use that path; it records and proves what exists. + Never ask for a key in the chat. + +When `/bin/ios-provision status` already returned `integrate_app` or +`verify_app`, say so and only confirm. + Then ask which features they want. Use the host's question tool (Claude Code: `AskUserQuestion` with `multiSelect: true`; Cursor: `AskQuestion` with `allow_multiple: true`), **at most 4 options per question**, otherwise plain diff --git a/iterable-react-native/SKILL.md b/iterable-react-native/SKILL.md index 6f1bc3b..b10b085 100644 --- a/iterable-react-native/SKILL.md +++ b/iterable-react-native/SKILL.md @@ -21,6 +21,17 @@ description: >- # Iterable React Native SDK +## Start here — do these in order, before anything else + +1. Find ``: from the real path of this `SKILL.md` (`cd -P "$(dirname )" && pwd -P`), walk up to the first folder that contains `bin/agent`. +2. From the app root, run `/bin/agent preflight` before you ask anything or edit anything. It reads local files only. +3. Do what its `next` says: open `next.open` if it is set, then run `next.commands` in order. + - `provision`: open `/iterable-provision/SKILL.md` now and follow it. Its first command is `/bin/agent question opening`. Write no code until it hands back. + - `integrate`: run `next.commands`, then do Step 0 and write the Android integration. + - `verify`: open `/iterable-android-verify/SKILL.md` and follow it. +4. Never write a placeholder, dummy, `.example` or hard-coded key, or a fake `google-services.json`. A missing input comes from provisioning, or you stop and say so. +5. Android first. Make no iOS edit until `iterable-android-verify` is green. Then, for iOS, run `/bin/ios-provision status --features ` and follow `iterable-provision`'s iOS path until its `next.kind` is `integrate_app`. + You are working with the **Iterable React Native SDK** — [`@iterable/react-native-sdk`](https://www.npmjs.com/package/@iterable/react-native-sdk). It is a JavaScript layer over Iterable's native Android and iOS SDKs. @@ -55,6 +66,18 @@ cost the developer a permission prompt. Do this **first**, before Preflight and before any edits. +**Prerequisites question — ask it in the first round, with the scope.** "Do you +already have the Iterable Mobile key, the Iterable mobile app with its push +integration, and a Firebase project with this app's `google-services.json`?" + +- **Not yet — set them up** (recommended): open `/iterable-provision/SKILL.md` + now and run `/bin/agent question opening`. +- **Yes, I have them**: still open `iterable-provision`; it finds, reuses and + proves what exists. Never ask for a key in the chat. + +When `/bin/agent preflight` already returned `integrate` or `verify`, the +files exist: say so and only confirm. + **Detect the workflow from the project, do not ask which docs to use.** - **Expo** if `app.json` / `app.config.js` / `app.config.ts` has an `expo` From d6df71238c833ef3dcee62c490bc55e3bc911c3e Mon Sep 17 00:00:00 2001 From: Franco Zalamena Date: Thu, 22 Oct 2026 14:53:16 +0100 Subject: [PATCH 13/15] Bump the plugin to 26.10.5-beta Co-Authored-By: Claude Opus 5.5 --- .claude-plugin/plugin.json | 2 +- .cursor-plugin/marketplace.json | 2 +- .cursor-plugin/plugin.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 9265995..e9cfab7 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "iterable-sdk", - "version": "26.10.4-beta", + "version": "26.10.5-beta", "description": "Takes an Android or iOS app — native or React Native — from nothing to a push notification proven on a device: Firebase or Apple (APNs) provisioning, version-pinned SDK integration, silent-failure pitfalls, and device-side proof that a push actually arrived. Android proof runs on a device or emulator; iOS proof runs on the iOS Simulator.", "author": { "name": "Iterable" }, "repository": "https://github.com/Iterable/iterable-sdk-skill", diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index 5390c5c..c06104b 100644 --- a/.cursor-plugin/marketplace.json +++ b/.cursor-plugin/marketplace.json @@ -3,7 +3,7 @@ "owner": { "name": "Iterable" }, "metadata": { "description": "Iterable's official skills for AI coding assistants — mobile SDK integration guidance.", - "version": "26.10.4-beta" + "version": "26.10.5-beta" }, "plugins": [ { diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index ead3696..cc01858 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "iterable-sdk", - "version": "26.10.4-beta", + "version": "26.10.5-beta", "description": "Takes an Android or iOS app — native or React Native — from nothing to a push notification proven on a device: Firebase or Apple (APNs) provisioning, version-pinned SDK integration, silent-failure pitfalls, and device-side proof that a push actually arrived. Android proof runs on a device or emulator; iOS proof runs on the iOS Simulator.", "author": { "name": "Iterable" }, "repository": "https://github.com/Iterable/iterable-sdk-skill", From 7a617ce0262cde55a1987a5c2c1265ac66c43f33 Mon Sep 17 00:00:00 2001 From: Franco Zalamena Date: Thu, 22 Oct 2026 15:20:23 +0100 Subject: [PATCH 14/15] Point ASK.md at the first unfinished step, pending or red ASK.md was written from the first red gate only, so a pending Iterable step (no server-side key yet) before a red install gate was skipped: the file said "waiting on you at App installed with the SDK" and the agent went on to write code. It now names the step next_action picks, the same one the banner and the agent path show. Co-Authored-By: Claude Opus 5.5 --- bin/config.sh | 15 +++++++++++++++ bin/gates | 14 +------------- tests/agent-next-action.sh | 18 ++++++++++++++++++ 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/bin/config.sh b/bin/config.sh index 4cce49f..592d8b6 100755 --- a/bin/config.sh +++ b/bin/config.sh @@ -1104,6 +1104,21 @@ next_part_banner() { # [gate name] [its verdict] # The one thing stopping the run, printed where nobody can scroll past it. Which # gate that is has already been decided by next_action — this is only how it looks, # so the styling can never disagree with the routing. +# ASK.md names the first unfinished step, pending or red, from next_action: the banner +# and the agent path follow the same source, so a pending step before the first red one +# is never skipped. +write_ask_md() { + local gate cmd summary + IFS="$NEXT_SEP" read -r _ _ gate cmd summary <<< "$(next_action)" + [[ -n "$gate" ]] || { gate="$1"; summary="$2"; } + { + printf '# ASK.md\n\nWaiting on you at **%s**, which only you can clear.\n\n %s\n' "$(gate_name "$gate")" "$summary" + [[ -z "$cmd" ]] || printf '\nStart it with:\n\n %s\n' "$cmd" + printf '\nDo that, then run this again — nothing already green will be redone:\n\n %s\n\n' "$(cmd_path onboard)" + printf '_Written %s. The tool overwrites this file each time it blocks._\n' "$(date '+%Y-%m-%d %H:%M')" + } > "$WS/ASK.md" +} + blocker_banner() { local owner kind gate cmd summary name colour label IFS="$NEXT_SEP" read -r owner kind gate cmd summary <<< "$(next_action)" diff --git a/bin/gates b/bin/gates index 392c029..a27592b 100755 --- a/bin/gates +++ b/bin/gates @@ -355,19 +355,7 @@ fi # needs you", 20 is "the tool found a real defect". Conflating them means a cron # loop cannot tell "waiting on a human" from "broken". if [[ -n "$HUMAN_FIX" ]]; then - cat > "$WS/ASK.md" </dev/null ok "$(printf '%-44s %s' "no gate reads it, and state.tsv is untouched" "gates unchanged")" || bad "the existing-push check reached the gates" "$(grep -l 'existing-push' bin/gates*)" +# A pending Iterable step comes before a later red one: the ladder must not send the +# agent past the dashboard steps to "install the app". +GREEN_TO_G9=() +for g in G0 G1 G2 G3 G4 G5 G6 G7 G8 G9; do GREEN_TO_G9+=("$(row "$g" green tool "$g" ok)"); done +state "${GREEN_TO_G9[@]}" \ + "$(row G10 pending human "Iterable API keys work" "no server-side key yet")" \ + "$(row G11 unverifiable human "Mobile app in Iterable" "human step")" \ + "$(row G12 unverifiable human "Push integration configured" "human step")" \ + "$(row G13 red human "App installed with the SDK" "this APK predates the integration")" +expect human iterable_keys "cmd:iterable-keys --step 1" "a pending Iterable step before a red install" +write_ask_md G13 "this APK predates the integration" +if grep -q "$(gate_name G10)" "$WS/ASK.md" && grep -q 'iterable-keys --step 1' "$WS/ASK.md" && + ! grep -q "$(gate_name G13)" "$WS/ASK.md"; then + ok "$(printf '%-44s %s' "ASK.md names the pending Iterable step" "G10")" +else + bad "ASK.md skipped the pending Iterable step" "$(cat "$WS/ASK.md")" +fi + echo ((FAILED)) && { echo " FAILED"; exit 1; } echo " All good — every state routes to one owner and one action." From 47ca21455121218cd3ffec9bffe9f2d08c87eed4 Mon Sep 17 00:00:00 2001 From: Franco Zalamena Date: Thu, 22 Oct 2026 15:36:31 +0100 Subject: [PATCH 15/15] Clear a stale ASK.md instead of rerouting it The ASK.md the agent followed in the Cursor run was left over from an earlier run. bin/gates writes it only when the first red step is the developer's; that run's first red step was the tool's (the FCM role), so the old file stayed and still named a later step. Rerouting its content through next_action did not touch that, and gave tool-owned steps "only you can clear" wording, so this goes back to the original content and deletes the file at the start of each run instead. This reverts commit 7a617ce02. Co-Authored-By: Claude Opus 5.5 --- bin/config.sh | 15 --------------- bin/gates | 17 ++++++++++++++++- tests/agent-next-action.sh | 18 ------------------ 3 files changed, 16 insertions(+), 34 deletions(-) diff --git a/bin/config.sh b/bin/config.sh index 592d8b6..4cce49f 100755 --- a/bin/config.sh +++ b/bin/config.sh @@ -1104,21 +1104,6 @@ next_part_banner() { # [gate name] [its verdict] # The one thing stopping the run, printed where nobody can scroll past it. Which # gate that is has already been decided by next_action — this is only how it looks, # so the styling can never disagree with the routing. -# ASK.md names the first unfinished step, pending or red, from next_action: the banner -# and the agent path follow the same source, so a pending step before the first red one -# is never skipped. -write_ask_md() { - local gate cmd summary - IFS="$NEXT_SEP" read -r _ _ gate cmd summary <<< "$(next_action)" - [[ -n "$gate" ]] || { gate="$1"; summary="$2"; } - { - printf '# ASK.md\n\nWaiting on you at **%s**, which only you can clear.\n\n %s\n' "$(gate_name "$gate")" "$summary" - [[ -z "$cmd" ]] || printf '\nStart it with:\n\n %s\n' "$cmd" - printf '\nDo that, then run this again — nothing already green will be redone:\n\n %s\n\n' "$(cmd_path onboard)" - printf '_Written %s. The tool overwrites this file each time it blocks._\n' "$(date '+%Y-%m-%d %H:%M')" - } > "$WS/ASK.md" -} - blocker_banner() { local owner kind gate cmd summary name colour label IFS="$NEXT_SEP" read -r owner kind gate cmd summary <<< "$(next_action)" diff --git a/bin/gates b/bin/gates index a27592b..fc863ce 100755 --- a/bin/gates +++ b/bin/gates @@ -12,6 +12,9 @@ FIRST_RED="" FAILED=0 STATE="$WS/state.tsv" ws_init; : > "$STATE" +# Written only when a run blocks on the developer, so one left by an earlier run would +# send whoever reads it to a step this run may not be on. +rm -f "$WS/ASK.md" # The spinner hides the cursor, and a hidden cursor outlives the process — Ctrl-C out of # a slow gate would otherwise leave the developer's own shell without one. INT is named @@ -355,7 +358,19 @@ fi # needs you", 20 is "the tool found a real defect". Conflating them means a cron # loop cannot tell "waiting on a human" from "broken". if [[ -n "$HUMAN_FIX" ]]; then - write_ask_md "$FIRST_RED" "$HUMAN_FIX" + cat > "$WS/ASK.md" </dev/null ok "$(printf '%-44s %s' "no gate reads it, and state.tsv is untouched" "gates unchanged")" || bad "the existing-push check reached the gates" "$(grep -l 'existing-push' bin/gates*)" -# A pending Iterable step comes before a later red one: the ladder must not send the -# agent past the dashboard steps to "install the app". -GREEN_TO_G9=() -for g in G0 G1 G2 G3 G4 G5 G6 G7 G8 G9; do GREEN_TO_G9+=("$(row "$g" green tool "$g" ok)"); done -state "${GREEN_TO_G9[@]}" \ - "$(row G10 pending human "Iterable API keys work" "no server-side key yet")" \ - "$(row G11 unverifiable human "Mobile app in Iterable" "human step")" \ - "$(row G12 unverifiable human "Push integration configured" "human step")" \ - "$(row G13 red human "App installed with the SDK" "this APK predates the integration")" -expect human iterable_keys "cmd:iterable-keys --step 1" "a pending Iterable step before a red install" -write_ask_md G13 "this APK predates the integration" -if grep -q "$(gate_name G10)" "$WS/ASK.md" && grep -q 'iterable-keys --step 1' "$WS/ASK.md" && - ! grep -q "$(gate_name G13)" "$WS/ASK.md"; then - ok "$(printf '%-44s %s' "ASK.md names the pending Iterable step" "G10")" -else - bad "ASK.md skipped the pending Iterable step" "$(cat "$WS/ASK.md")" -fi - echo ((FAILED)) && { echo " FAILED"; exit 1; } echo " All good — every state routes to one owner and one action."