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.11-beta",
"version": "26.10.12-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.11-beta"
"version": "26.10.12-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.11-beta",
"version": "26.10.12-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
7 changes: 4 additions & 3 deletions INTENDED-FLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ Numbered because the auditor reports divergence by waypoint.
**W1 — Scope.** First prompt, the agent establishes what the developer wants integrated and
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, any existing-push choice, the data region, and the JWT policy to the developer
and their picks are on disk (the same region and JWT questions native iOS asks).
identity, any existing-push choice, the data region, and the JWT policy to the developer,
their picks are on disk, and they have confirmed the whole **Scope** summary (same beat as
native iOS `confirm_scope`).
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.
prints the scope screen, and a status read reports `scope`, until Step 0 is confirmed.

**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
35 changes: 26 additions & 9 deletions bin/agent
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,14 @@ if [[ "${1:-}" == preflight ]]; then
SUMMARY="$SUMMARY Android is finished. If iOS is in scope, start it with the command below$([[ -n "${SCOPE_FEATURES:-}" ]] && printf ', which carries the recorded Step 0 answers' || printf ' and the --features from Step 0')."; }
fi
# Ahead of both: provisioning asks for the test identity, and code is built on the scope.
SCOPE_MISSING="$(scope_missing)"
SCOPE_MISSING="$(scope_gate)"
if [[ -n "$SCOPE_MISSING" && ( "$KIND" == provision || "$KIND" == integrate ) ]]; then
KIND=scope OPEN=""; DO=("$BIN/agent question scope")
SUMMARY="Step 0 is not recorded yet. Write no code and start no setup. Put each screen to the developer, run the commands of the options they pick, then the command each one names next, until preflight returns another kind."
if [[ "$SCOPE_MISSING" == confirm ]]; then
SUMMARY="Step 0 answers are on disk but not confirmed. Put the Scope summary to the developer; Keep records the whole scope, then run preflight again. Write no code and start no setup until preflight returns another kind."
else
SUMMARY="Step 0 is not recorded yet. Write no code and start no setup. Put each screen to the developer, run the commands of the options they pick, then the command each one names next, until preflight returns another kind."
fi
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},' \
Expand Down Expand Up @@ -241,8 +245,13 @@ case "${1:-status}" in
jwt-check-clear)
keys=SCOPE_JWT_CHECK v="" ;;
clear)
keys="SCOPE_FEATURES SCOPE_IDENTITY SCOPE_PUSH SCOPE_REGION SCOPE_JWT SCOPE_JWT_CHECK" v="" ;;
*) printf '{"error":%s}\n' "$(jstr "usage: agent scope feature <ids> | identity email|user-id|unknown-user | existing-push keep-both|iterable-only | region US|EU | jwt true|false | jwt-check | jwt-check-clear | clear")"; exit 30 ;;
keys="SCOPE_FEATURES SCOPE_IDENTITY SCOPE_PUSH SCOPE_REGION SCOPE_JWT SCOPE_JWT_CHECK SCOPE_CONFIRMED" v="" ;;
confirm)
if [[ -n "$(scope_missing)" ]]; then
printf '{"error":%s}\n' "$(jstr "scope is not complete yet — finish the scope screens before confirming")"; exit 30
fi
keys=SCOPE_CONFIRMED v=true ;;
*) printf '{"error":%s}\n' "$(jstr "usage: agent scope feature <ids> | identity email|user-id|unknown-user | existing-push keep-both|iterable-only | region US|EU | jwt true|false | jwt-check | jwt-check-clear | confirm | clear")"; exit 30 ;;
esac
for k in $keys; do
kv="$v"
Expand All @@ -261,9 +270,17 @@ case "${1:-status}" in
fi
printf -v "$ek" '%s' "$ev"
fi
M="$(scope_missing)"
printf '{"recorded":%s,"file":%s,"next":{"command":%s}}\n' "$(scope_json "$M")" "$(jstr "$WS/resolved.env")" \
"$(jstr "$BIN/agent $([[ -n "$M" ]] && printf 'question scope' || printf preflight)")"
if [[ "${1:-}" != confirm ]]; then
save_resolved SCOPE_CONFIRMED "" && SCOPE_CONFIRMED=""
fi
G="$(scope_gate)"
if [[ "${1:-}" == confirm ]]; then
printf '{"recorded":%s,"confirmed":true,"file":%s,"next":{"command":%s}}\n' \
"$(scope_json "")" "$(jstr "$WS/resolved.env")" "$(jstr "$BIN/agent preflight")"
else
printf '{"recorded":%s,"file":%s,"next":{"command":%s}}\n' "$(scope_json "$G")" "$(jstr "$WS/resolved.env")" \
"$(jstr "$BIN/agent $([[ -n "$G" ]] && printf 'question scope' || printf preflight)")"
fi
exit 0
;;
set)
Expand Down Expand Up @@ -403,9 +420,9 @@ IFS="$NEXT_SEP" read -r NEXT_OWNER NEXT_KIND NEXT_GATE NEXT_CMD NEXT_SUMMARY <<<
# 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
if [[ -n "$(scope_gate)" ]]; 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."
NEXT_SUMMARY="Step 0 is not finished yet. Put the scope screens to the developer before any setup starts; confirm the whole scope, then run $BIN/agent question opening again."
fi ;;
esac

Expand Down
18 changes: 13 additions & 5 deletions bin/questions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,14 @@ scope_missing() {
fi
}

# What still blocks provisioning and code: a missing answer, or the whole-scope yes.
scope_gate() {
local m
m="$(scope_missing)"
[[ -n "$m" ]] && { printf '%s' "$m"; return; }
[[ "${SCOPE_CONFIRMED:-}" == true ]] || printf confirm
}

scope_jwt_keys_url() {
if [[ "${SCOPE_REGION:-}" == EU ]]; then
printf 'https://app.eu.iterable.com/settings/apiKeys'
Expand Down Expand Up @@ -416,9 +424,9 @@ EOF
)"
printf '"requires":[],"recommended":%s,"options":[' "$(jstr "Keep these answers")"
q_opt "Keep these answers" \
"Nothing changes, and preflight names the next step." \
"nothing changed" "" \
"$BIN/agent preflight"
"Records the whole scope, then preflight names the next step." \
"SCOPE_CONFIRMED recorded" "" \
"$BIN/agent scope confirm"
printf ','
q_opt "Change the answers" \
"Clears the recorded answers, and the features screen comes back." \
Expand Down Expand Up @@ -1340,7 +1348,7 @@ question_for() {
scope_more) q_scope_more ;;
# 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 ;;
opening) if [[ -n "$(scope_gate)" ]]; 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 @@ -1354,7 +1362,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) if [[ -n "$(scope_missing)" ]]; then q_scope; else q_opening; fi ;;
choose_driver) if [[ -n "$(scope_gate)" ]]; 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
7 changes: 4 additions & 3 deletions iterable-android/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,10 @@ when the app already handles push itself whether to keep it, the Iterable
project region (US or EU), and whether the Mobile key requires JWT (same
screens as native iOS provisioning). Each option's command records its answer
in `.iterable/resolved.env`; EU also records `ITBL_BASE` in `.iterable/.env`.
Preflight's `scope` field reports them, and `question scope` offers to change
them once all are in. Never pick an option for the developer. Prerequisites are
preflight's to find: never ask for a key in the chat.
Preflight's `scope` field reports them. When every field is in, `question scope` shows
the **Scope** summary; **Keep these answers** runs `agent scope confirm` before
provisioning. Any change clears confirmation and asks again. Never pick an option for
the developer. Prerequisites are preflight's to find: never ask for a key in the chat.

Do **not** assume the developer wants every feature, and do **not** start
implementing on a guessed scope.
Expand Down
14 changes: 8 additions & 6 deletions iterable-provision/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,12 +178,14 @@ 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.
**Step 0 comes first.** Until the developer has confirmed the whole scope, that command
prints a scope screen (**Features**, **Identity**, region, JWT, then the **Scope**
summary) 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 **Keep these answers** records it (`agent scope confirm`) and
preflight names provisioning. Then run `<root>/bin/agent question opening` again: with
the scope confirmed 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
Expand Down
7 changes: 4 additions & 3 deletions iterable-react-native/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,10 @@ when the app already handles push itself whether to keep it, the Iterable
project region (US or EU), and whether the Mobile key requires JWT (same
screens as native iOS provisioning). Each option's command records its answer
in `.iterable/resolved.env`; EU also records `ITBL_BASE` in `.iterable/.env`.
Preflight's `scope` field reports them, and `question scope` offers to change
them once all are in. Never pick an option for the developer. Prerequisites are
preflight's to find: never ask for a key in the chat.
Preflight's `scope` field reports them. When every field is in, `question scope` shows
the **Scope** summary; **Keep these answers** runs `agent scope confirm` before
provisioning. Any change clears confirmation and asks again. Never pick an option for
the developer. Prerequisites are preflight's to find: never ask for a key in the chat.

**Detect the workflow from the project, do not ask which docs to use.**

Expand Down
2 changes: 1 addition & 1 deletion tests/agent-preflight.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ echo
# The Step 0 answers, as `agent scope` records them; tests/agent-scope.sh covers that gate.
scoped() {
mkdir -p "$1/.iterable"
printf 'SCOPE_FEATURES=push-notifications,sdk-initialization\nSCOPE_IDENTITY=email\nSCOPE_REGION=US\nSCOPE_JWT=false\n' >> "$1/.iterable/resolved.env"
printf 'SCOPE_FEATURES=push-notifications,sdk-initialization\nSCOPE_IDENTITY=email\nSCOPE_REGION=US\nSCOPE_JWT=false\nSCOPE_CONFIRMED=true\n' >> "$1/.iterable/resolved.env"
}

A="$TMP/fresh"; new_app "$A"
Expand Down
2 changes: 1 addition & 1 deletion tests/agent-questions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
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 SCOPE_REGION=US SCOPE_JWT=false
export SCOPE_FEATURES=push-notifications,sdk-initialization SCOPE_IDENTITY=email SCOPE_PUSH=keep-both SCOPE_REGION=US SCOPE_JWT=false SCOPE_CONFIRMED=true
cd "$(dirname "$0")/.."

FAILED=0
Expand Down
24 changes: 17 additions & 7 deletions tests/agent-scope.sh
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,14 @@ proven() { # <dir> — provisioning proven, nothing placed in the app yet
}
recorded() { # <dir> <features> <identity> [existing-push]
mkdir -p "$1/.iterable"
printf 'SCOPE_FEATURES=%s\nSCOPE_IDENTITY=%s\nSCOPE_REGION=US\nSCOPE_JWT=false\n' "$2" "$3" >> "$1/.iterable/resolved.env"
printf 'SCOPE_FEATURES=%s\nSCOPE_IDENTITY=%s\nSCOPE_REGION=US\nSCOPE_JWT=false\nSCOPE_CONFIRMED=true\n' "$2" "$3" >> "$1/.iterable/resolved.env"
[[ -n "${4:-}" ]] && printf 'SCOPE_PUSH=%s\n' "$4" >> "$1/.iterable/resolved.env"
}
finish_scope() { # <dir> — region and JWT after features, identity, and optional push
in_app "$1" scope region US >/dev/null
in_app "$1" scope jwt false >/dev/null
}
confirm_scope() { in_app "$1" scope confirm >/dev/null; }
in_app() { local d="$1"; shift; ( cd "$d" && "$ROOT/bin/agent" "$@" 2>/dev/null ); }
field() { node -e 'let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{const j=JSON.parse(d);const v=process.argv[1].split(".").reduce((o,k)=>o?.[k],j);process.stdout.write(typeof v==="object"?JSON.stringify(v):String(v))})' "$1"; }
cmds() { node -e 'let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{for(const o of JSON.parse(d).options)console.log(o.label+"\t"+o.commands.join(" ; "))})'; }
Expand Down Expand Up @@ -148,12 +149,20 @@ o="$(in_app "$A" scope identity user-id)"
|| bad "region was not next after identity: $o"
finish_scope "$A"
o="$(in_app "$A" scope jwt false)"
[[ "$(field next.command <<< "$o")" == "$ROOT/bin/agent question scope" \
&& "$(field recorded.missing <<< "$o")" == confirm ]] \
&& ok "after the last scope answer, the next screen is the whole-scope summary" \
|| bad "the last scope answer did not route to the summary: $o"
[[ "$(field header <<< "$(in_app "$A" question scope)")" == Scope ]] \
&& ok "preflight stays on scope until the summary is confirmed" \
|| bad "preflight allowed setup before scope confirmation"
confirm_scope "$A"
OUT="$(in_app "$A" preflight)"
[[ "$(field next.command <<< "$o")" == "$ROOT/bin/agent preflight" && "$(field next.kind <<< "$OUT")" == provision \
[[ "$(field next.kind <<< "$OUT")" == provision \
&& "$(field next.commands.0 <<< "$OUT")" == "$ROOT/bin/agent question opening" \
&& "$(field scope.identity <<< "$OUT")" == user-id ]] \
&& ok "with features and identity recorded, the next command is preflight, and it routes to provisioning" \
|| bad "the recorded scope did not hand on to provisioning: $o / $OUT"
&& ok "after Keep, preflight routes to provisioning" \
|| bad "the confirmed scope did not hand on to provisioning: $OUT"
grep -qF "not even while the developer works in a dashboard" <<< "$(field next.summary <<< "$OUT")" \
&& ok "and the provisioning summary forbids code while a dashboard step is open" \
|| bad "the provision summary still allows code during the dashboard steps"
Expand All @@ -162,10 +171,11 @@ R="$(in_app "$A" question scope)"
[[ "$(field header <<< "$R")" == Scope ]] && grep -qF "Identity: user-id" <<< "$(field prompt <<< "$R")" \
&& grep -qF "Region: US" <<< "$(field prompt <<< "$R")" && grep -qF "JWT: not required" <<< "$(field prompt <<< "$R")" \
&& grep -qF "$ROOT/bin/agent scope clear" <<< "$(cmds <<< "$R")" \
&& grep -qF "$ROOT/bin/agent scope confirm" <<< "$(cmds <<< "$R")" \
&& ok "once recorded, the scope screen shows the answers and offers to change them" \
|| bad "no way to see or change the recorded answers: $R"
in_app "$A" scope clear >/dev/null
[[ -z "$(rec "$A" SCOPE_FEATURES)$(rec "$A" SCOPE_IDENTITY)$(rec "$A" SCOPE_REGION)$(rec "$A" SCOPE_JWT)" \
[[ -z "$(rec "$A" SCOPE_FEATURES)$(rec "$A" SCOPE_IDENTITY)$(rec "$A" SCOPE_REGION)$(rec "$A" SCOPE_JWT)$(rec "$A" SCOPE_CONFIRMED)" \
&& "$(in_app "$A" preflight | field next.kind)" == scope ]] \
&& ok "changing them clears every scope field and routes back to scope" || bad "clear left: $(cat "$A/.iterable/resolved.env")"

Expand Down Expand Up @@ -199,8 +209,8 @@ X="$(in_app "$N" question scope)"
XC="$(cmds <<< "$X")"
grep -qF "scope existing-push keep-both" <<< "$XC" && grep -qF "scope existing-push iterable-only" <<< "$XC" \
&& in_app "$N" scope existing-push keep-both >/dev/null && [[ "$(rec "$N" SCOPE_PUSH)" == keep-both ]] \
&& finish_scope "$N" && [[ "$(in_app "$N" preflight | field next.kind)" == provision ]] \
&& ok "keep both or Iterable only is recorded, then region and JWT, and provisioning comes next" \
&& finish_scope "$N" && confirm_scope "$N" && [[ "$(in_app "$N" preflight | field next.kind)" == provision ]] \
&& ok "keep both or Iterable only is recorded, then region and JWT, confirm, and provisioning comes next" \
|| bad "the push answer did not record or route: $XC"

N2="$TMP/native-no-push-scope"; native_app "$N2"
Expand Down
2 changes: 1 addition & 1 deletion tests/agent-transitions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
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 SCOPE_REGION=US SCOPE_JWT=false
export SCOPE_FEATURES=push-notifications,sdk-initialization SCOPE_IDENTITY=email SCOPE_PUSH=keep-both SCOPE_REGION=US SCOPE_JWT=false SCOPE_CONFIRMED=true
cd "$(dirname "$0")/.."

FAILED=0
Expand Down
2 changes: 1 addition & 1 deletion tests/text-surface.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
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 SCOPE_REGION=US SCOPE_JWT=false
export SCOPE_FEATURES=push-notifications,sdk-initialization SCOPE_IDENTITY=email SCOPE_PUSH=keep-both SCOPE_REGION=US SCOPE_JWT=false SCOPE_CONFIRMED=true
cd "$(dirname "$0")/.."

FAILED=0
Expand Down
Loading