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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "iterable-sdk",
"version": "26.10.8-beta",
"version": "26.10.9-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",
Expand Down
2 changes: 1 addition & 1 deletion .cursor-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"owner": { "name": "Iterable" },
"metadata": {
"description": "Iterable's official skills for AI coding assistants — mobile SDK integration guidance.",
"version": "26.10.8-beta"
"version": "26.10.9-beta"
},
"plugins": [
{
Expand Down
2 changes: 1 addition & 1 deletion .cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "iterable-sdk",
"version": "26.10.8-beta",
"version": "26.10.9-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",
Expand Down
2 changes: 2 additions & 0 deletions INTENDED-FLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ Numbered because the auditor reports divergence by waypoint.
confirms it. On Android it is recorded, not remembered: `agent preflight` returns `scope`
ahead of provisioning and code until `agent question scope` has put the features, the
identity and any existing-push choice to the developer and their picks are on disk.
An agent that enters through provisioning meets the same gate: `agent question opening`
prints the scope screen, and a status read reports `scope`, until those picks are on disk.

**W2 — The Android work.** The agent writes config, init and identity, guided by the skill
and `PITFALLS.md`. It never fakes an input to make a build pass: no placeholder
Expand Down
11 changes: 11 additions & 0 deletions bin/agent
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,17 @@ esac
NEXT="$(next_action)"
IFS="$NEXT_SEP" read -r NEXT_OWNER NEXT_KIND NEXT_GATE NEXT_CMD NEXT_SUMMARY <<< "$NEXT"

# Same gate as preflight's, for an agent that entered through provisioning: nothing in the
# Google or Iterable half starts until Step 0 is recorded. Device steps are not gated,
# because by then the code exists and the scope was asked before it was written.
case "$NEXT_KIND" in
choose_driver|run_in_terminal|authenticate|choose_target|project_unreachable|approve_firebase|register_app|provision|iterable_keys)
if [[ -n "$(scope_missing)" ]]; then
NEXT_OWNER=human NEXT_KIND=scope NEXT_CMD="$BIN/agent question scope"
NEXT_SUMMARY="Step 0 is not recorded yet. Put the scope screens to the developer before any setup starts; then run $BIN/agent question opening again."
fi ;;
esac

# The same step, in words a developer who has never read this repo can use. `gate`
# stays because the state file and our own vocabulary are keyed on it, but an agent
# that has only an id to quote says "G13" to somebody it means nothing to.
Expand Down
6 changes: 4 additions & 2 deletions bin/questions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1234,7 +1234,9 @@ question_for() {
case "${1:-}" in
scope) q_scope ;;
scope_more) q_scope_more ;;
opening) q_opening ;;
# Provisioning asks for the test identity the scope decides, so the scope comes first
# even when an agent enters here instead of through the platform skill's preflight.
opening) if [[ -n "$(scope_missing)" ]]; then q_scope; else q_opening; fi ;;
# Two kinds, one fork. next_action reports `run_in_terminal` when the developer is
# the driver and `approve_firebase` when the agent is, because the terminal path
# asks for consent in person and needs no agent to collect it. Asked in a chat
Expand All @@ -1248,7 +1250,7 @@ question_for() {
# The fork itself, reported while it is still unanswered. Same screen as the opening
# one because it is the same question — a second, smaller version of it would be a
# second wording to keep true, and the one that goes stale is whichever they read.
choose_driver) q_opening ;;
choose_driver) if [[ -n "$(scope_missing)" ]]; then q_scope; else q_opening; fi ;;
sign_in|authenticate) q_sign_in ;;
# The door, then the picker, and they are different questions: one asks whether the
# listing may happen, the other shows what came back. next_action calls both
Expand Down
8 changes: 8 additions & 0 deletions iterable-provision/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,13 @@ credentials, reads nothing, and writes nothing — not even a workspace director
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.

**Step 0 comes first.** Until the developer's scope is recorded, that command prints a
scope screen (**Features**, then **Identity**) instead of the setup question, and a
status read reports `next.kind: scope`. Put each one to the developer, run the commands
of the options they pick, and follow each `next.command` until it names `preflight`.
Then run `<root>/bin/agent question opening` again: with the scope recorded it is the
setup question. Never record a scope answer the developer did not pick.

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. The usual mistake is to decide provisioning
Expand Down Expand Up @@ -339,6 +346,7 @@ relay the block, do not look for a flag that gets you past it, and never set

| `next.kind` | What you do |
|---|---|
| `scope` | Step 0 is not recorded. Put `next.question` to the developer, run the commands of the option they pick, and follow each `next.command` until it names `preflight`. Then ask `agent question opening`. |
| `choose_driver` | Nobody has said who runs the Google half yet. Ask `next.question` (`agent question opening`) and wait for their pick. **Do not relay a terminal block here** — that is one of the two answers. |
| `run_in_terminal` | They chose their own terminal. Relay the handoff block verbatim, say you'll wait, stop. |
| `install_tools` | Report exactly which binaries are missing (`missing_tools`) and what each unlocks: `gcloud` the Google half, `adb` the device half, `node` the JSON parsing. Do not install them. |
Expand Down
5 changes: 4 additions & 1 deletion tests/agent-questions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
# No network, no credentials, no project.

set -uo pipefail
# Step 0 is recorded: these checks are about the setup screens, which the scope gate
# holds back until it is (tests/agent-scope.sh covers that gate).
export SCOPE_FEATURES=push-notifications,sdk-initialization SCOPE_IDENTITY=email SCOPE_PUSH=keep-both
cd "$(dirname "$0")/.."

FAILED=0
Expand Down Expand Up @@ -433,7 +436,7 @@ ok "every step leaves 'that did not work' as an answer that changes nothing"
# so the identity, an email or a user ID and not a secret, is the only value in part 2
# collected that way.
for n in 1 2 3 4; do
ft="$(q iterable_step "$n" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const q=JSON.parse(s);console.log(q.free_text?q.free_text.command:"")})')"
ft="$(SCOPE_IDENTITY= q iterable_step "$n" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const q=JSON.parse(s);console.log(q.free_text?q.free_text.command:"")})')"
case "$n" in
4) [[ "$ft" == *"set ITBL_IDENTITY="* ]] \
&& ok "step 4 takes the test identity as typed text — an email or user ID, not a secret" \
Expand Down
29 changes: 29 additions & 0 deletions tests/agent-scope.sh
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,35 @@ done
&& ok "every dashboard step's prompt says no app code is written while it is open" \
|| bad "the prompt says nothing about writing code meanwhile on step(s):$NOCODE"

echo
echo " Entering through provisioning still asks the scope first"
echo

P="$TMP/provision-first"; native_app "$P"
O="$(in_app "$P" question opening)"
[[ "$(field header <<< "$O")" == Features && ! -d "$P/.iterable" ]] \
&& ok "with no scope, the provisioning skill's first question is the features screen, and nothing is written" \
|| bad "question opening with no scope: header $(field header <<< "$O")"
[[ "$(in_app "$P" question choose_driver | field header)" == Features ]] \
&& ok "the setup fork is held behind the scope too" || bad "choose_driver asked before the scope"
recorded "$P" push-notifications,sdk-initialization email
in_app "$P" scope existing-push keep-both >/dev/null
O="$(in_app "$P" question opening)"
[[ "$(field header <<< "$O")" != Features && "$(field header <<< "$O")" != Identity && -n "$(field header <<< "$O")" ]] \
&& ok "with the scope recorded, the same command is the setup question" \
|| bad "question opening with a scope: header $(field header <<< "$O")"

S="$TMP/status-first"; native_app "$S"
O="$(in_app "$S")"
[[ "$(field next.kind <<< "$O")" == scope && "$(field next.command <<< "$O")" == "$ROOT/bin/agent question scope" \
&& "$(field next.question.header <<< "$O")" == Features ]] \
&& ok "a status read before the scope reports scope, with the features screen as its question" \
|| bad "status with no scope: $(field next.kind <<< "$O") / $(field next.command <<< "$O")"
recorded "$S" push-notifications,sdk-initialization email
in_app "$S" scope existing-push keep-both >/dev/null
[[ "$(in_app "$S" | field next.kind)" != scope ]] \
&& ok "with the scope recorded, a status read routes to setup again" || bad "status still gated after the scope"

echo
echo " Every scope screen can be drawn"
echo
Expand Down
3 changes: 3 additions & 0 deletions tests/agent-transitions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
# project "looked at" and the screens branch on whether it has been.

set -uo pipefail
# Step 0 is recorded: these checks are about the setup screens, which the scope gate
# holds back until it is (tests/agent-scope.sh covers that gate).
export SCOPE_FEATURES=push-notifications,sdk-initialization SCOPE_IDENTITY=email SCOPE_PUSH=keep-both
cd "$(dirname "$0")/.."

FAILED=0
Expand Down
3 changes: 3 additions & 0 deletions tests/text-surface.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
# see from the text they are editing.

set -uo pipefail
# Step 0 is recorded: these checks are about the setup screens, which the scope gate
# holds back until it is (tests/agent-scope.sh covers that gate).
export SCOPE_FEATURES=push-notifications,sdk-initialization SCOPE_IDENTITY=email SCOPE_PUSH=keep-both
cd "$(dirname "$0")/.."

FAILED=0
Expand Down
Loading