Skip to content

Fixes from real-app trials: ask once, doc-backed decisions, clearer iOS proof - #32

Merged
franco-zalamena-iterable merged 17 commits into
mainfrom
feat/ios-scope-carry
Oct 22, 2026
Merged

franco-zalamena-iterable merged 17 commits into
mainfrom
feat/ios-scope-carry

Conversation

@franco-zalamena-iterable

Copy link
Copy Markdown
Contributor

Summary

These fixes come from running the skill as a client would on real open-source apps: the official Mastodon iOS app (UIKit, with its own push relay and Notification Service Extension) and a bare React Native app. The Mastodon run ended green, with a push proved on the iOS Simulator. Along the way it showed where the skill asked twice, recommended from guesswork, or hid what the developer was agreeing to.

  • Ask once, record once (native iOS).
    • The iOS skill's Step 0 asks scope and identity with the app in view. It passes the answers as ios-provision status --features … --identity ….
    • A single carry_scope screen records them, so provisioning no longer asks again.
    • The trial's second ask had silently dropped Mobile Inbox/Embedded. A test now pins that case.
  • Decisions come from shipped docs.
    • Seven Iterable articles now ship in each platform's corpus, through the pipeline: User ID rules, project types and unique identifiers, reserved profile fields, anonymous users, creating and identifying users, changing an email or userId, and the EU data center. React Native also gets identifying-the-user.
    • Each skill's Step 0 has a "Decisions: read before recommending" table. A recommendation must cite a shipped doc, or isn't offered.
    • Identity asks the project type first, with an "I don't have a project yet / I don't know, help me" path.
    • userIds follow the doc: the app's stable ID or a random UUID, never an email, handle or other PII.
    • tests/decision-docs.sh fails CI when a table cites a doc or heading that doesn't ship.
  • iOS screens show what the developer agrees to. Confirm, approve and replace screens now carry their summary in prompt, not only in context, matching the Android fix in [SDK-800] Provision and prove React Native apps on Android and iOS #31. In the trial, the scope confirmation showed only "Use this Iterable integration scope?".
  • The proof tests the right build and the right user.
    • A new App build step recommends the newest build that contains the SDK. In the trial, the installed copy was the pre-integration build.
    • The signed-in identity is offered as one tap, not typed. The integration adds a temporary ITBL_PROOF identity= log line, which prints only when the app is launched with -iterableProofIdentity YES. The proof reads it, and offers it only after Iterable confirms this Simulator's current device token is on that user.
    • The marker is removed after the proof, and inspect warns if it's left in.
  • Verify reads registration from Iterable, not logs. SDK 6.7.5 logs a successful token registration only at debug level, which its default logger drops. The skills and I12 now say to judge registration only from ios-verify status (new pitfall 29).
  • An app's own push is a choice, not a blocker.
    • Step 0, on all three platforms, asks: keep the app's own notifications alongside Iterable's, or use Iterable only?
    • inspect, and the Android done line, detect an existing push path: the app's own callbacks or extension, a FirebaseMessagingService, or OneSignal, Braze and similar.
    • When one is found, the verdict offers an optional after-integration check. Iterable's proof alone completes the integration.
  • Test fixtures use neutral names. No project names remain anywhere in the repo, tests/ and eval/ included.

Stacked on #31.

Reviewing it

  1. Carry the iOS skill's Step 0 scope and identity into iOS provisioning.
  2. Ship the Iterable docs that identity and region decisions depend on. Pipeline configs plus the generated reference/ files.
  3. Route Step 0 decisions to the doc that settles them.
  4. Fail CI when a Step 0 decision cites a doc that doesn't ship.
  5. Put iOS screen summaries in the prompt and bind the newest proof build.
  6. Judge iOS registration from Iterable and flag an app's own push path.
  7. Offer the app's own push check as optional, and ask keep-or-replace first. Android parity, with a shared bin/existing-push.js.
  8. Bump the plugin to 26.10.4-beta.
  9. Offer the proof identity the device shows, once Iterable ties it to the Simulator. The ITBL_PROOF marker, --identity-candidate, and the token-verified offer.
  10. Use neutral app names in test fixtures and eval scenarios. Same-length replacements, so length-sensitive fixtures are untouched. This includes tools/screens.

How it was tested

  • Suites: each change ran its own suites locally, and each new guard was proved load-bearing (removing it fails a test). Commit 9 skipped its final guard-proof batches to save time, and the fixture rename was checked against the suites that read those fixtures. The full suite runs in CI on this PR.
  • Live (Mastodon iOS, iPhone 18 Pro Simulator, iOS 27.0, on [SDK-800] Provision and prove React Native apps on Android and iOS #31's build): provisioning, integration and verify reached done with a nonce-matched delivery. The fixes here address every friction point that run surfaced.
  • simctl push and Notification Service Extensions: a probe app on the iOS 26.2 and 27.0 Simulators showed simctl push never runs an extension. That's why the app's own push check is developer-triggered.

Known limitations

  • iterable-docs #1569 must watch the new trees. It doesn't yet watch the trees the new articles live in, so they won't auto-refresh until it does. A patch is ready.
  • Pipeline rendering. It leaves some :::v-pre blocks and list-indented callouts raw, and doesn't strip "Want to learn more?" sections, in a few articles, old and new.
  • No shipped doc names the native logout API. The Logout row cites the nearest sections.
  • JWT. The docs recommend JWT for client-side keys, while the provisioning walks default to No JWT. The table cites the doc; the policy is still to be decided.

🤖 Generated with Claude Code

A urlHandler that returns false re-opens the URL: Android fires ACTION_VIEW
pinned to the app itself, so a link on the app's own scheme or tracking
domain loops, offline too, because a failed redirect passes the tracking URL
on. The allowed-protocols pitfall claimed http was implicit; only https is,
and the match is case-sensitive on the text before "://". The routing rows
sent allowed protocols to the UUA article and deep links to the
Branch/AppsFlyer stub.

The skill now has a compiled App Links snippet with the 3.7.0 names,
autoVerify, and the Iterable prerequisites, and new pitfalls 24-25.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A React Native app now finishes Android (provisioned, integrated, and proved
by iterable-android-verify) before iOS starts. The iOS phase reuses the
Android setup and the scope confirmed in Step 0, which the skill passes as
--features. The app is integrated when every platform in scope has passed
its verifier.

For bare iOS the skill owns the native push code: the AppDelegate token and
notification callbacks, a native notification-permission request at launch,
and universal links in the 0.88 template's SceneDelegate. It gives the exact
Release Simulator build and install commands, with signing on, because a live
trial installed a stale build found with find and built with code signing
off, which strips aps-environment. It loads the Mobile key the way
provisioning delivers it: an optional require on bare React Native and
EXPO_PUBLIC_ITERABLE_API_KEY on Expo, where push sets
requestPermissionsForPushNotifications.

Install resolves the latest version with one npm view that survives a scoped
private registry, and never prints an npmrc. The New Architecture is always on
from 0.82, so the installing article's opt-out is skipped. Deep links no
longer loop through Linking.openURL. The Xcode 27 notes live in pitfall 12,
and the skill and its regenerated corpus are pinned to SDK 3.2.1. New pitfalls
16-22.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bin/ios-project.sh answers where the app is, where its Xcode project is, and
which kind of app it is: native, react-native, or expo. Every iOS actor
resolves through it. Source scans prune node_modules, Pods and build output,
so the SDK's own sources no longer pass for the app's integration, and
inspect reads React Native signals where they live.

The proof artifact for React Native is a Release build: a Debug Simulator
build has no main.jsbundle and runs only while Metro serves it.

Before giving any push advice, I6 and ios-prove now check the app actually
installed on the Simulator. It must contain the Iterable SDK (and, for
React Native, a bundle that references RNIterableAPI), carry aps-environment,
match the recorded build, and not be older than the source. A live trial
reinstalled a pre-integration build four times, and ios-prove's first answer
sent it hunting for a permission problem. inspect warns when an Expo app
never asks for notification permission.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The iOS provisioning path writes the Mobile key where a JavaScript app reads
it: .iterable/rn/iterable.local.ts on bare React Native, and
EXPO_PUBLIC_ITERABLE_API_KEY in a gitignored .env.local on Expo. Native apps
keep the xcconfig.

When the Android program already ran, iOS offers import-from-android. One
Mobile and one Server-side key serve both platforms, with the Android region,
No JWT and test email, validated again for iOS. The search walks up to the git
toplevel, so a monorepo app finds the workspace kept at the root.

The scope the React Native skill confirmed arrives as --features. It is shown
on the import and confirmation screens before anything is written, and
recorded with its source. restart --app-path now binds a fresh build to an
existing proof run, and a missing .app is refused before anything is
archived.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A live trial's agent never printed context on any Android screen, so the
developer saw "Which key is on your clipboard?" with no steps. The Firebase
approval also asked them to agree to "the changes listed above" with no list
on screen. The steps, links, values, and the list of changes being approved
now go in the prompt, with the question last, as on iOS. Steps 1 and 2 link to
app.eu.iterable.com for an EU base, and "show again" re-asks the question
instead of printing into a tool result nobody sees.

The package screens recommend the applicationId read from the app's app.json
or build.gradle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In the Mastodon iOS trial, iterable-ios Step 0 asked features and identity
with the app in view. Then ios-provision asked both again. The second
Features answer dropped Mobile Inbox and Embedded, so the recorded scope
contradicted Step 0. The generic identity help steered an app whose
signed-in session has no email toward Email. The skill said provisioning
"can conduct" the conversation, and native iOS had no way to hand Step 0's
answers to the tool; only the React Native import path could.

status and question now take --features and --identity as read-only
proposals. Values the tool's own screens would not record are refused
before anything is written. With no Android setup to import, one
carry_scope screen names exactly what Step 0 confirmed. Its recommended
option runs carry-scope, which records them through the same writer
import-from-android now uses. Each carried field keeps its own source, so
confirm_scope marks each line's origin, scope.json keeps it, and a later
answer drops only that field's attribution. Change the answers records the
decline and falls back to the normal screens.

The source follows the app's framework, ios-step0 or react-native-step0. A
native app next to an Android setup is therefore not labeled as the React
Native skill, and its carried identity wins over the Android test email.
Without the flags the flow and scope.json are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The skills recommended identifiers without the rules behind them: nothing
shipped defined project types, the userId rules (no PII, no handles, not
guessable, 128 characters) or the EU data center. Add those articles to all
three platform corpora at each config's existing pin, plus the
identifying-the-user article React Native was missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A live trial recommended setUserId("<accountID>@<instance>"), a handle,
because nothing pointed the agent at Iterable's userId rules when it chose.
Each skill's Step 0 now asks the project type first (with a "help me" path
that inspects the app and recommends), has existing clients reuse the
identifier their other systems send, and holds a decisions table: before
recommending, read the named section and cite it, or offer no
recommendation.

The Android per-install UUID default is now the account-less case only,
with the reinstall caveat and the doc it follows. Routing rows the table
replaces are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tests/decision-docs.sh parses each skill's decisions table and checks that
every slug has a file in that platform's reference/ (or the sibling path
React Native uses), every named section is a heading in it, and every row
names at least one doc. A mutated copy of the iOS table proves it can fail.
run-all.sh picks it up with the other suites.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A live trial integrating Iterable into the Mastodon iOS app never printed
context, so the developer confirmed the integration scope seeing only "Use
this Iterable integration scope?": the Features, Identity, Region and JWT
lines lived only in context. The proof summary, the Android-reuse consent,
the App Store Connect plan's side effects, every rejected-key reason, the
detected Bundle ID and Team ID, and the missing-tool steps had the same gap.
As on Android, they now sit in the prompt with the question last, context
keeps background only, and no option points "above" at text it does not show.

The proof start defaulted to the installed app, which was the pre-Iterable
baseline build. When the workspace knows a build of the app that carries the
SDK, recorded by an earlier run or under the project's -derivedDataPath, a new
App build screen recommends binding that exact .app unless the installed copy
was built later; the installed app stays an option. An existing run offers a
fresh run bound to a newer build.

The developer also had to type a 36-character user ID with no way to find
it. No device or Iterable source yields it reliably: the SDK keeps it in the
Keychain and never logs it, and the public API has no lookup by device token.
So it stays typed, and the screen says where to read it, down to a Simulator
debugger read of the running app's IterableAPI.userId.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A live trial integrated Iterable into the Mastodon iOS app, which already had
its own push relay and Notification Service Extension. Two verification gaps
showed up.

The agent read the Unified Log, saw the APNs token and no registerToken line,
and decided the device was not registering. SDK 6.7.5 reports a successful
registration only at debug level, and the default log delegate drops debug,
so that line never appears. I12's next step also told agents to inspect
"registration logs". Both iOS skills now say registration is read from
Iterable through ios-verify status. Pitfall 29 records the SDK facts, and the
I12 step points at the device record and says to re-run after a rebuild.

Nothing proved Mastodon's own pushes still worked after its shared callbacks
and extension were edited, and done did not list that as unproved. inspect now
detects an existing non-Iterable push path: callbacks that do more than
forward to Iterable, an extension that hands back content itself, or another
push SDK. It warns about it. With push in scope, status keeps
existing-push-coexistence in feature_evidence_required, so
integration_complete stays false. Rule 8 gains the closing step. A probe on
the iOS 26.2 and 27.0 Simulators showed simctl push never runs the extension,
so it cannot stand in for that proof.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…irst

Franco's rule: once Iterable's push is proved, the integration counts as done.
The app's own push must not hold up `done`. ab9d5cc listed
existing-push-coexistence in feature_evidence_required, which kept
integration_complete false. On iOS it now goes in verification_limitations
instead, and done offers an optional after-integration check that the app's
own notifications still show. It runs only when the developer asks for it.

The real decision belongs earlier, so Step 0 now asks it on iOS, Android, and
React Native whenever existing push is found: keep the app's own
notifications alongside Iterable's, or use Iterable notifications only. Keep
both uses rule 8's forwarding. Iterable only means proposing, in the one
diff the developer confirms, how the old provider stops handling pushes.
Android's and React Native's rule 8 "never replace" now names that confirmed
diff as its one exception.

Android gets the same optional line. bin/existing-push.js now holds the
detection for both platforms, so they share one list of React Native push
libraries. On Android it flags an app FirebaseMessagingService that does more
than forward to Iterable, a MESSAGING_EVENT service that is not Iterable's,
and push SDKs in Gradle. Only next_action's done summary reads it, so no gate
changes and config.sh grows by five lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he Simulator

In the Mastodon trial the integration agent found the signed-in user ID in
the Simulator's log: the SDK's default logger echoes Iterable's "No user
exists with userId <id>" for a request sent before the user existed. The
proof screen still made Franco type the 36-character ID, so he copied it from
the agent's message. Typing added friction and no safety.

The identity screen now gathers candidates from the device and the agent:
the integration's temporary ITBL_PROOF marker, the SDK's own no-user lines,
and --identity-candidate. It offers one only when Iterable holds this
Simulator's current APNs token on that user, the same read and token match
the verifier's I12 gate makes, and records it only when the developer taps
it. With no candidates it makes no request. A relaunch option restarts the
app with -iterableProofIdentity YES so the marker logs the identity it signs
in with.

The iOS and React Native skills add that marker behind the launch argument,
not #if DEBUG, because the proved build can be Release. They remove it after
the proof, and inspect and the done summary warn proof_identity_marker_present
until it is gone. The verify skill now describes the App build screen and the
offered identity instead of a hand-appended --app-path and a typed answer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fixtures and scenarios named the test apps the skill was trialled on.
They now use same-length neutral names, so length-sensitive fixtures and
digests are untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@franco-zalamena-iterable
franco-zalamena-iterable marked this pull request as ready for review October 22, 2026 12:48
@franco-zalamena-iterable
franco-zalamena-iterable merged commit bbb084d into main Oct 22, 2026
3 of 4 checks passed
@franco-zalamena-iterable
franco-zalamena-iterable deleted the feat/ios-scope-carry branch October 22, 2026 13:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant