From 3929cc4b6716f474a08a10f1dbc36728626ec438 Mon Sep 17 00:00:00 2001 From: Franco Zalamena Date: Sun, 25 Oct 2026 21:24:08 +0000 Subject: [PATCH] Require whole-scope confirmation before Android provisioning Step 0 fields can be on disk without the developer seeing the full picture. Add SCOPE_CONFIRMED and scope_gate so preflight and question opening stay on the Scope summary until Keep runs agent scope confirm, matching iOS confirm_scope. Co-authored-by: Cursor --- .claude-plugin/plugin.json | 2 +- .cursor-plugin/marketplace.json | 2 +- .cursor-plugin/plugin.json | 2 +- INTENDED-FLOW.md | 7 ++++--- bin/agent | 35 ++++++++++++++++++++++++--------- bin/questions.sh | 18 ++++++++++++----- iterable-android/SKILL.md | 7 ++++--- iterable-provision/SKILL.md | 14 +++++++------ iterable-react-native/SKILL.md | 7 ++++--- tests/agent-preflight.sh | 2 +- tests/agent-questions.sh | 2 +- tests/agent-scope.sh | 24 +++++++++++++++------- tests/agent-transitions.sh | 2 +- tests/text-surface.sh | 2 +- 14 files changed, 83 insertions(+), 43 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 8284e67..f808541 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.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", diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index 3f3fd1b..0d1152e 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.11-beta" + "version": "26.10.12-beta" }, "plugins": [ { diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index c8bb958..a2a9c73 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -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", diff --git a/INTENDED-FLOW.md b/INTENDED-FLOW.md index 2c8bb52..7ea56ba 100644 --- a/INTENDED-FLOW.md +++ b/INTENDED-FLOW.md @@ -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 diff --git a/bin/agent b/bin/agent index 57d224d..f3aba06 100755 --- a/bin/agent +++ b/bin/agent @@ -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},' \ @@ -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 | 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 | 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" @@ -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) @@ -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 diff --git a/bin/questions.sh b/bin/questions.sh index 0eb6091..e033549 100644 --- a/bin/questions.sh +++ b/bin/questions.sh @@ -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' @@ -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." \ @@ -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 @@ -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 diff --git a/iterable-android/SKILL.md b/iterable-android/SKILL.md index 72f5084..3498f75 100644 --- a/iterable-android/SKILL.md +++ b/iterable-android/SKILL.md @@ -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. diff --git a/iterable-provision/SKILL.md b/iterable-provision/SKILL.md index 58ea7b7..d5960cb 100644 --- a/iterable-provision/SKILL.md +++ b/iterable-provision/SKILL.md @@ -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 `/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 `/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 diff --git a/iterable-react-native/SKILL.md b/iterable-react-native/SKILL.md index e79a371..987446b 100644 --- a/iterable-react-native/SKILL.md +++ b/iterable-react-native/SKILL.md @@ -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.** diff --git a/tests/agent-preflight.sh b/tests/agent-preflight.sh index 6b6c348..52bf57a 100755 --- a/tests/agent-preflight.sh +++ b/tests/agent-preflight.sh @@ -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" diff --git a/tests/agent-questions.sh b/tests/agent-questions.sh index b314eb2..e6aa046 100644 --- a/tests/agent-questions.sh +++ b/tests/agent-questions.sh @@ -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 diff --git a/tests/agent-scope.sh b/tests/agent-scope.sh index 004e5a4..288af1b 100755 --- a/tests/agent-scope.sh +++ b/tests/agent-scope.sh @@ -49,13 +49,14 @@ proven() { # — provisioning proven, nothing placed in the app yet } recorded() { # [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() { # — 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(" ; "))})'; } @@ -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" @@ -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")" @@ -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" diff --git a/tests/agent-transitions.sh b/tests/agent-transitions.sh index 38c3968..ccccd02 100755 --- a/tests/agent-transitions.sh +++ b/tests/agent-transitions.sh @@ -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 diff --git a/tests/text-surface.sh b/tests/text-surface.sh index 0f4d6be..2d12de7 100755 --- a/tests/text-surface.sh +++ b/tests/text-surface.sh @@ -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