92 KiB
name: store-pitfalls description: Known pitfalls with App Store Connect, Google Play and storebound itself, learned from real releases, and what to do about each - first: do the work, don't ask the owner for every step; store lists that lag right after a create, dry runs that print a count instead of a plan, App Store names taken on a storefront, keyword limits in characters (not bytes), Data safety answers without code evidence, missing font glyphs in screenshots, console sign-in and sessions, Capacitor monorepo plugins, duplicates from retries and re-runs, search ranks that are only a trend, no Play search API, Play's Create app form (no API, opener and submit share a name, Free is final), App Privacy purposes over-mapped (Play advertising is Third-Party Advertising only), an unknown console page, a signed-in console read as a sign-in page, console forms driven agent-first, stale Capacitor web assets, a declined flow step logged ok. Read it before any store task, and when a store command does something you did not expect.
Store pitfalls
Each entry: symptom → cause → what to do → what guards it now. The full changelog of lessons with dates is
docs/pitfalls.md in the storebound repository. When you hit a new one, tell the owner and add it there (and here).
First: do it, don't ask (the costliest pitfall)
You and storebound do everything: API calls, uploads, builds, CSV / template exports, every console navigation
(open, Get Started, Edit, Next, Continue, Save draft), console forms, verification. Never hand the owner "run this" or
"click X". The owner presses only the final public or irreversible buttons (Submit for review, release or a
production rollout, App Privacy Publish, legal agreements; deleting store data, live prices, review replies,
testers) and answers, in ONE batch at the start, only the facts no file proves (compliance answers that check /
validate list). Loop: plan, drive, verify, continue. Full rule: docs/agents-core.md (AGENTS.md); lesson 26.
1. A product you just created is "not there"
- Symptom:
apple iap-reviewuploads nothing for a productapple iapcreated a minute ago; a list looks short. - Cause: App Store Connect's
/inAppPurchasesV2list (and Play's product list) can lag for a short while after a create. A command that walks the list skips what is not in it yet. - Do: wait a minute and re-run. Never assume a product was handled because the command finished.
- Guard:
apple iap-reviewcompares the config's products with the store's list, names the missing ones ("the store's list can lag a minute after creating; re-run") and exits 1.apple iapandplay productsread the list again after creating and warn about any new product that is not in it yet; their dry runs name products that are not on the store.
2. A dry run that only prints a count
- Symptom:
--drysays "listing: 20 locales" or "3 change(s)", but not which fields would change to what. - Cause: the command logged a summary instead of the plan.
- Do: never ask the owner to approve a count. Show the rows that change (item, action, old → new). If a command
only prints a count, say so and show the owner the diff from
pull/--jsonoutput instead. - Guard:
cli/lib/dry.tsdryPlan(title, rows)prints the standard plan table (apple iap,apple iap-review,play productsuse it);tests/pitfalls.test.tsscanscli/**for--drybranches that print nothing but a count.
3. "The app name you entered is already being used"
- Symptom: creating the App Store record or saving the name fails although you own no app with that name.
- Cause: App Store app names are unique per storefront, across all developers.
- Do: ask the owner for a variant (a short suffix such as "Example Notes: Lists") and set
apple.namein the config; keepapp.namefor Play and the brand. Do not pick the suffix yourself. - Guard:
apple.nameoverrides the App Store name.
4. Keyword field length (superseded by 43)
- The limit is 100 characters, not UTF-8 bytes (see 43). Count characters; drop whole words, not commas.
5. Data safety says "email collected" for an app that never asks for one
- Symptom: the proposal marks email (or phone) as collected because the app has accounts.
- Cause: account features do not imply email collection (anonymous ids, Game Center, Play Games sign-in).
- Do: answer only what the code proves; ask the owner for the rest (rule 5).
- Guard:
storebound scanmarks email only with evidence (an email field, an email sign-up call) and otherwise says "ask owner".
6. Boxes or wrong glyphs in localized screenshots
- Symptom: Turkish, Polish or Vietnamese captions show fallback glyphs (ş, ğ, ł, ệ) while the English ones are fine.
- Cause: the font (or the Google Fonts subset that was downloaded) does not cover those characters, and the browser silently draws them with another font.
- Do: check every language's characters before rendering, pick a font that covers them or let the fallback fill in.
- Guard:
storebound shots fonts/shots checkcheck each language's characters against the fonts' glyphs.
7. Console automation stops at sign-in or loses the session
- Symptom:
console fillwaits on a sign-in page, cannot find fields, or is signed out on the next run. - Cause: the App Store Connect sign-in runs inside an
idmsa.apple.comiframe; "User Access" is a plain heading, not a labeled group; Chrome drops session cookies unless the last session is restored. - Do: let the owner sign in in the opened window (never type credentials); re-run after the sign-in.
- Guard: the console session handles the iframe and the plain heading and starts Chrome with
--restore-last-session; a partial miss fills every field found and leaves only the missing ones to the owner.
8. scan misses Capacitor plugins in a monorepo
- Symptom: SDKs or privacy manifests of Capacitor plugins are not found.
- Cause: the plugins live in
apps/native/node_modules(a workspace package), not in the rootnode_modules. - Do: run
scanfrom the project root; check that the stack line lists the plugins. - Guard:
storebound scanandprivacy-manifestresolve packages through workspace folders too.
9. A second run (or a retry) creates a duplicate
- Symptom: two in-app purchases or subscription groups with the same name, a screenshot twice in a set, an offer code campaign or a custom product page twice, after a timeout, a crash, a re-run a minute later, or two runs at once.
- Cause: a POST create retried after a timeout or a 5xx may have been applied the first time; the store's lists lag right after a create, so a re-run does not see what the last run made; two writers race; a Play edit redone after a commit whose answer was lost.
- Do: just run the command again: every write command is an upsert and converges (
docs/idempotency.md). Never retry a create by hand (storebound api) without a GET first. If a run says another storebound run holds the lock, wait for it; use--force-unlockonly when that run is really gone. Never run two write commands for the same app in parallel. - Guard:
cli/lib/upsert.ts(findOrCreate: fresh re-check + journal.storebound/created.json; createOnce: 409 = found, an unclear answer is looked up, never re-sent blindly), retries only for requests that are safe twice, Play edits deleted on every error and never redone after an unclear commit, screenshot / preview sets keep the matching prefix in order,.storebound/locks/per app and store.tests/idempotency.test.ts.
10. "We rank #4 for this keyword" (from the Search API)
- Symptom:
aso ranksays #4, the owner's phone shows the app lower (or higher) in App Store search. - Cause: the iTunes Search API order is close to, not equal to, App Store search: no personalisation, no search ads, no in-app event rows. It has no subtitle field either.
- Do: read ranks as a trend over weeks (
aso auditarrows), never as a promise; never claim a competitor's subtitle (no public API has it); a popularity number exists only from Apple Ads, never estimate one. - Guard:
aso rank,aso competitorsandaso auditsay it in their output;aso suggestshows popularity "unknown" without Apple Ads credentials.
11. "Check our Google Play keyword ranks"
- Symptom: the owner asks for Play ranks or Play competitors per keyword.
- Cause: Google Play has no public search API; reading Play search pages would be scraping.
- Do: say so. Measure Play through the Play Console acquisition reports (search terms) and
analytics snapshot(store listing visitors, acquisitions); improve the listing text withaso audit(short description, density). - Guard:
asocovers App Store search only; the aso-basics skill states the limit.
12. A new Play app: "Create app" is both the opener and the submit
- Symptom: an agent cannot create the Play app through the API, or clicks "Create app" without knowing whether it opens the form or submits it; or an app goes out as Free and the owner later wants it paid.
- Cause: the Play Developer API cannot create apps. Play Console's Home has a "Create app" button that opens the
form, and the form's final button has the same words. Free cannot become Paid later on Google Play. The form
(
/console/u/0/developers/<developerId>/create-new-app, verified 2026-10-12) has an App package name field (max 150) with a read-only "Check availability" button ("Package name available"); the package name is fixed for good. App or game and Free or paid are<material-radio>s: setting the native input leaves the form saying "Select app or game" until the inner[role=radio]is clicked (a JS click works in a background window). - Do:
storebound console plan play-app-recordand drive the form with your browser tool (or the scriptedconsole fill play-app-record); fill the package name and press Check availability; click the inner[role=radio]s and check that no "Select …" hint is left (console verify play-app-record); tell the owner that Free is final; the owner ticks the declarations and presses Create app. Afterwards the app lives at/console/u/0/developers/<developerId>/app/<appId>/app-dashboard(setplay.appId). The service account's app-level permissions do NOT cover the new app: the OWNER adds it (Users and permissions > the service account > Add app > Admin, or the minimal set > Apply > Save changes; never an agent: it is a permission grant), thenstorebound apps check --store play. - Guard:
createAppAllowedlets the opener through only on the app list, never inside a form or dialog (English and Turkish); agreement checkboxes are never ticked; Free or paid is outlined as an owner check.
13. App Privacy purposes over-mapped
- Symptom: App Privacy says Developer's Advertising or Marketing for Device ID, Coarse Location, Product Interaction or Performance Data, although the app only shows an ad SDK's ads.
- Cause: a Play purpose was turned into two Apple purposes ("advertising" -> Third-Party Advertising AND Developer's Advertising or Marketing). Apple's definitions (developer.apple.com/app-store/app-privacy-details/): third-party ads are Third-Party Advertising; Developer's Advertising is first-party ads or marketing to your users.
- Do: one Apple purpose per Play purpose; put the owner's real answers in
appPrivacy.types(Apple type ids, purposes, linked, tracking) and letstorebound checkcompare them with Info.plist and PrivacyInfo.xcprivacy. - Guard:
applePurpose(cli/lib/declarations.ts),appPrivacy.typesvalidation andappPrivacyMismatches(cli/lib/app-privacy.ts),tests/app-privacy.test.ts.
14. An unknown console page: bounded wait and a debug dump
- Symptom:
console fillclicked Next (or opened the form) and then nothing happened in the log for a long time. - Cause: the console showed a page storebound does not recognise (a changed layout, a new question): no field it can fill and no button it may click.
- Do: read the warning: it names the page ("the page after "Next""), its buttons, and where the debug dump is.
Drive that part yourself with your browser tool from
storebound console plan <form>(agent-first), then go on. To report it, re-run with--debug-dumpand attach the folder.storebound/console-debug/<form>-<time>/after the owner looked atscreen.jpg(the screenshot shows what the window showed).page.json/page.htmlhold labels and structure only: no values, cookies or tokens; emails, phone numbers and the account area are replaced. - Guard: the watchdog in
fillSession(--step-timeout, default 30 s; a heartbeat line a minute; 20 min without progress ends the run with exit 1),cli/console/debug.ts,tests/browser/console-flow.browser.ts(unknown-page fixture and a scan for waits without a deadline).
15. App Privacy data types not found in the real dialog
- Symptom: after Get Started and Next,
console fill apple-app-privacyfinds no data type checkbox. - Cause: the real "Data Collection" dialog names each checkbox by its label run together with its help text ("NameIncluding first or last name"), and some labels differ from Apple's help page ("Purchases", "Other Data").
- Do: nothing by hand: storebound matches the label as a prefix. If the console changes again, the watchdog
(pitfall 14) writes a debug dump: re-run with
--debug-dumpand attach it. - Guard:
labelNamesandTYPE_ALIASES(cli/lib/console-forms.ts),tests/browser/console-flow.browser.ts.
16. "Still waiting for the sign-in" on a console that is signed in
- Symptom:
console fill(often with the copied Chrome cookies) prints only "still waiting for the sign-in" while the window shows Play Console (the account chooser, the app list). - Cause: Google pages embed
accounts.google.com/ogs.google.comframes (the account widget, cookie rotation) while the owner is signed in; an older detector counted any frame on a sign-in host as a sign-in page. - Do: read the line "sign-in detected because: at " and the heartbeats (they name the page). A
top-level
accounts.google.com/idmsa.apple.com/ App Store Connect/loginURL right after the launch means the copied Chrome session did not carry over (expired or tied to that device): sign in once in the window, or run without--use-chrome-session(pitfall 18). Anything else: re-run with--debug-dump(up to 3 dumps of the sign-in wait) and attach the folder after the owner looked atscreen.jpg. - Guard:
signInReasonincli/console/fill.ts(top-level URL, the main frame's password / email-entry form, a visible Apple sign-in frame; no other frame counts), the sign-in watchdog dump,tests/browser/console-fill.browser.ts.
17. App Privacy data types listed but never set up
- Symptom: after the data types were saved,
console fill apple-app-privacystops on the App Privacy page with every type showing "Set Up " and nothing clicked. - Cause: the real page shows "Set Up " as plain text (a
<p>in a div with a click handler: no button, no link) in a box after the Data Types heading, so role-based lookups miss it. - Do: nothing by hand: storebound clicks each "Set Up " by its exact text (only for types it answers, never
the "?" help buttons or Publish) and fills that type's dialog step by step. A dialog step it does not know: it writes
a debug dump, says
paused on "<type>", and goes on once the owner answered that step. - Guard:
setUpAllowed(cli/lib/console-forms.ts),setUpClick/setUpType(cli/console/fill.ts),tests/browser/console-flow.browser.tswithtests/fixtures/console/apple-app-privacy-setup.html.
18. Never copy Google session cookies: they are device-bound
- Symptom:
console fill play-app-recordwith the owner's copied Chrome cookies reached the Play account chooser once (22:32); between runs Google showedaccounts.google.com/v3/signin/rejected; from 23:12 the same copy landed on the account chooser. - Cause: Google binds its session cookies to the device. Chrome's Device Bound Session Credentials keep a key in the TPM / Secure Enclave that cannot leave the machine; Google only renews the short-lived cookies for the browser that proves it holds that key (public on Windows from Chrome 146, rolling out on macOS from Chrome 147). Google also signs out sessions whose cookies look stolen ("logged out of their account for that session and any related suspicious sessions on that device", then an extra verification). Copied cookies in a second Chrome look exactly like cookie theft: the copy is refused, and the owner's REAL Chrome session can be signed out or the account asked to verify again.
- Do: never copy Google cookies. The default is storebound's own profile (
~/.storebound/console-profile/<store>): the owner signs in there once, by hand; it is kept.--use-chrome-session googleexists only as an explicit owner opt-in that prints the warning; an agent never adds it. Apple publishes no such binding, but--use-chrome-session appleis opt-in too. If Google says "Couldn't sign you in" in storebound's window, storebound prints "Google refused sign-in in this window (/signin/rejected)": try again, or sign in at accounts.google.com first in that window. - Confirmed (2026-10-12): the earlier default copy of the Google cookies DID sign the owner out of Play Console in their real Chrome.
- Guard:
browserPlan/openConsoleBrowserincli/console/session.ts(no copy without the flag, the copy keeps only the form's own provider), Chrome started by storebound without--enable-automationand withnavigator.webdriverfalse,googleRejected;tests/browser/console-session.browser.ts. - Sources: https://blog.google/security/protecting-cookies-with-device-bound-session-credentials/ , https://developer.chrome.com/docs/web-platform/device-bound-session-credentials , https://knowledge.workspace.google.com/admin/security/investigate-and-take-action-on-suspicious-session-cookies
19. play images aborts on missing tablet screenshots
- Symptom:
storebound play images --dryon a phone-only project (renders with feature, ios65, ios69 and play, no tab7 / tab10) stopped withen-US: no sevenInchScreenshots in .../store/renders/en (render first: storebound shots render). - Cause: every Play image type was treated as required. Play requires only the 512 x 512 icon, the 1024 x 500 feature graphic and at least 2 screenshots (Play Console Help, "Add preview assets to showcase your app", answer 9866151); 7" / 10" tablet, TV and Wear screenshots are optional (without tablet screenshots the app is not eligible for tablet featuring).
- Do: nothing special:
play imagesuploads what exists and printstablet screenshots: none (optional; ...). To render and require tablets setplay.tablets: true; to never ask for them,play.tablets: false(default: true when tablet renders exist). A missing optional set never deletes the tablet screenshots Play already has; the dry run showskeeps sevenInchScreenshots on Play, and only--prune(owner's call) deletes them. - Also: the next abort was
GET .../listings/de-DE/icon -> 404 "Listing for language 'de-DE' not found.": 20 languages were stillstatus=draft, soplay listinghad (rightly) not created them.play imagesnow takes the same languages asplay listing(drafts skipped unless--include-drafts) and skips a language with no Play listing (skipped de-DE: no Play listing yet), never failing on it. Runplay listingbeforeplay images. - Guard:
langImages/missingChangeincli/play/images.ts,resolvedTabletsincli/shots/report.ts;tests/play-post-reads.test.ts(phone-only fixture: dry plan, info line, exit 0;tablets: truewithout renders is an error; no tablet DELETE without--prune).
20. Drive consoles agent-first; the scripted fill is a fast path
- Symptom: hours went into fix cycles of the scripted engine's selectors (
console fill) while the console UI kept differing from the model (App Privacy's "Set Up" as plain text, unlabeled purpose checkboxes, two information steps, Play's material radios), each one a stop until a new release. - Cause: a script only knows the page it was written for. An AI agent with a browser tool reads the page as it is and adapts.
- Do: run
storebound console plan <form> --jsonand drive the page yourself (Claude in Chrome, a built-in browser, Playwright) from its answer sheet, never-press list and UI facts; long flows as an in-page async loop that writeswindow.__log(one evaluate call times out around 45 s); stop before the final button and tell the owner what to press; thenstorebound console verify <form>.console fillis the optional scripted fast path; when it stalls, switch to the plan instead of waiting for a selector fix. - Guard:
console plan/console verify(CLI and the read-only MCP toolsconsole_plan,console_verify), the console-autofill skill puts them first;tests/console-plan.test.ts.
21. Play Data safety: conditional required rows
- Symptom:
play data-safetyanswers 400 "Invalid safety labels declaration: Response missing for PSL_ACCOUNT_DELETION_URL" (or anotherPSL_...id) although--drylooked fine. - Cause: some questions are required only after another answer. Account creation not
PSL_ACM_NONErequires the delete-account URL (PSL_ACCOUNT_DELETION_URL) and the data-only deletion question (PSL_SUPPORT_DATA_DELETION_BY_USER; when yes,PSL_DATA_DELETION_URL), and Play then refusesPSL_HAS_OUTSIDE_APP_ACCOUNTS. A selected data type needs its whole usage block. The CSV must also use the current form's Response IDs:PSL_SUPPORT_DATA_DELETION_BY_USERbecame a single choice (DATA_DELETION_YES/DATA_DELETION_NO/DATA_DELETION_NO_AUTO_DELETED) and a bare FALSE got 400 "Line 7: Response ID missing";PSL_DATA_COLLECTION_USER_REQUEST_DELETEwas removed from the form. - Do: set
accounts.deletionUrl(it also answersPSL_SUPPORT_DATA_DELETION_BY_USERwith yes: that is the general "can users request deletion?" question, not "without deleting the account"; a no next to a delete-account URL was a false declaration storebound wrote once), setpartialDeletionUrlonly for a data-only page, runstorebound check/play data-safety --dryuntil nomissing: <id>is left. If Play names an id storebound does not know, export the form (Play Console > App content > Data safety > Export to CSV) and add the row indataSafety.extra(same id = replaces the generated row). When Play changes the form, export it empty and save it overcli/data/play-data-safety-form.csv. - Guard:
DATA_SAFETY_RULES/typeRules/dataSafetyIssuesincli/lib/data-safety.ts;check,project.declarationsandplay data-safety(dry and real, before the POST) report them. storebound checks every (Question ID, Response ID) pair,dataSafety.extraincluded, against Google's exported empty form (cli/data/play-data-safety-form.csv, 2026-10-12;formPairIssues) and takes each Answer requirement from it;tests/pure.test.ts(pairs from Play's exported form). - Sources: https://support.google.com/googleplay/android-developer/answer/10787469 (Data safety form, CSV import), https://support.google.com/googleplay/android-developer/answer/13327111 (account deletion: delete-account link; deleting other data is a separate question).
Adding a lesson
Every bug found in real use gets an entry in docs/pitfalls.md and here, and, where possible, a check or a test
that fails without the fix (see CONTRIBUTING.md).
22. play build: "AAB manifest without package / versionCode" on a good bundle
- Symptom: Gradle printed BUILD SUCCESSFUL, then storebound stopped with
AAB manifest without package / versionCode(storebound before this fix).play uploadandplay releaseused the same reader. - Cause: storebound read the AAB's proto manifest (aapt2 Resources.proto) with the wrong field numbers. Nothing was wrong with the bundle.
- Do: update storebound. Don't rebuild or change the Gradle setup for this error.
- Guard:
aabInfoincli/lib/gradle.ts; a byte-level test that doesn't use storebound's own encoder.
23. Capacitor: native build ships old web assets
- Symptom:
storebound flow run release --applywent straight intoapple build(xcodebuild archive) whileios/App/App/publicandandroid/app/src/main/assets/publicwere 3.5 hours older than the latest source commit: the build would have shipped the old game. - Cause: a Capacitor app's native projects contain a copy of the web build that
npx cap syncmade last. Neither xcodebuild nor Gradle rebuilds or re-copies it; nothing ran the web build + cap sync before the native build. - Do: set
build.prebuild(e.g."npm run build && npx cap sync"; per storeapple.prebuild/play.prebuild).apple build/play buildrun it first in the project root (printed, output streamed, a nonzero exit stops the build;--dryonly prints it;--no-prebuildskips it); inside oneflow runthe shared command runs once, not per store. Never pass--allow-stale-webto get past the check without the owner knowing that the old web build ships. - Guard: after the prebuild, a Capacitor project's copied
public/index.htmlmust be newer than the last git commit outside ios/ android/ node_modules/ store/ .storebound/ and than<webDir>/index.html, else the build stops with "web assets are older than the source (copied ..., source ...)" (webStep/webStalenessincli/lib/web-assets.ts).storebound checkwarns when the assets are stale and no prebuild is set. The build output and the dry-run plan show the web bundle that goes in (sha256 of the copied index.html and its mtime).tests/web-assets.test.ts. - Monorepo: with the Capacitor app in
apps/native(its own capacitor.config.ts, android/, ios/),play buildonce failed with "no Gradle project (settings.gradle) in android or .". The builds, the staleness check andchecknow find the one Capacitor project one or two levels down (workspaces, apps/, packages/) and name it; several are listed and needplay.build.dir/apple.build.workspace|project(capacitorProjectincli/lib/web-assets.ts).
24. A flow step that declined its confirmation was logged "ok"
- Symptom:
flow run release --applyranstorebound apple upload --no-wait, which printed "not a terminal: re-run with --yes to confirm" and uploaded nothing, yet the flow logged "4/13 upload: ok" and went on toapple testflight waitfor a build that was never uploaded. - Cause: a declined confirmation (
confirm()incli/lib/ctx.ts) returned from the command with exit 0, andflow run --yeswas not passed on to the steps, so a run without a terminal could never confirm. - Do: read a step's output, not only its status. After showing the dry run, run the writes yourself with
flow run <name> --apply --yes(owner steps never run in a flow, they print as "YOUR CLICK"). - Guard: every declined or unanswerable confirmation prints
declined: <reason>and exits 3 (DECLINED_EXIT);flow runstops with "step N declined: " (statusdeclined, neverok) and adds--yesto write steps only with--apply --yes(planStepincli/lib/flow.ts,declinedReasonincli/flow.ts).tests/flow.test.ts(non-TTY flow with a confirming step and no--yesfails at that step with "declined").
25. Builds and uploads in a non-interactive shell: no SDK, no output
- Symptom:
storebound play buildstopped in Gradle with "SDK location not found. Define ANDROID_HOME or sdk.dir in local.properties" although the SDK was in~/Library/Android/sdk.storebound apple upload --no-wait --yesran for over 10 minutes with no line at all while it sent the IPA. - Cause: agents and scripts run non-interactive shells:
ANDROID_HOMEfrom.zshrcis not set there, andlocal.properties(where Android Studio writessdk.dir) is gitignored, so a fresh checkout has none. The upload logged only every 10th part and had no timeout per part or per API call, so one large or stalled part was silence. - Do: nothing special now.
play buildfinds the SDK like Android Studio (ANDROID_HOME, ANDROID_SDK_ROOT,sdk.dirin local.properties, the Android Studio default folder per OS), passes it to Gradle in the environment and never writes local.properties; without one it stops before Gradle and lists what it checked (storebound doctorshows the SDK and its source).apple uploadprints the size, a line per part and a heartbeat every 20 s (also for altool and the processing wait);--timeoutstops it with exit 7, and running the same command again resumes. Still run long uploads in the background with a log and report its lines. - Guard:
resolveAndroidSdk/androidSdkEnvincli/lib/tools.ts,cli/play/build.ts,cli/doctor.ts;sendPartsincli/apple/upload.ts,cli/lib/progress.ts(injectable heartbeat),waitForBuildincli/apple/testflight.ts, a 2-minute limit per App Store Connect call (ASC_REQUEST_TIMEOUT_MS);tests/tools-path.test.ts,tests/build-sign.test.ts,tests/upload-progress.test.ts.
26. Asking the owner for every step
- Symptom: an agent using storebound spent hours handing the owner one click, one command or one answer at a time: "open App content", "press Next", "download the CSV template", "now run the upload", "what is your answer for X?" in the middle of a run.
- Cause: skills and rules said "let the owner do it" and "by hand" for navigation, draft saves, exports and uploads, and compliance questions were asked when the run reached them, not at the start.
- Do: follow the first rule (top of this file,
docs/agents-core.md): do every API call, upload, build, export, console navigation and form yourself, verify by reading back, go on. Collect every fact no file proves in one batch at the start (storebound check/validatelist them) and save the answers in the config. Stop only for the owner steps (Submit for review, release / production rollout, App Privacy Publish, legal agreements, deleting store data, live prices, review replies, testers), prepared with the dry run shown. Makecheck/--drycatch store refusals so they never surface as a 400 mid-run. - Guard: the rule is the first section of
docs/agents-core.mdand so of every generated adapter (AGENTS.md, GEMINI.md, Copilot and Cursor rules), of README and of this skill;tests/do-it-dont-ask.test.tschecks that and fails when a skill or the rules hand navigation, draft saves, exports, uploads or form filling to the owner (navigation "by hand", uploads or exports left to the owner, asking the owner to click through a form).
27. Play steps without an API: category, countries, App content, IARC
- Symptom: after a first Play release through the API the App category showed "Not selected", production had 0
countries, and an agent hunted for each App content card; the IARC questionnaire kept revealing new questions, an
evaluate call timed out at 45 s, material checkboxes ignored
form_input, and "Send changes for review" ignored a JS click. - Cause:
play listingcannot set the category (edits.details has no category field) and edits.countryAvailability is read-only; the IARC questionnaire is one long page where each Yes reveals follow-ups; Play's<material-checkbox>/<material-radio>keep their own model. - Do: set
play.category,play.countries(or all),console.contentRating.details,console.appContent,socialandproducts[].randomRewardsfrom ONE batch of owner answers (storebound console plan play-app-contentlists them), then driveconsole plan play-store-settings,play-app-contentand its cards,play-production-countriesandplay-production-release. Click the inner[role=radio]/[role=checkbox]; run long flows with the plan'sinPageLoopand pollwindow.__log. Never tick the IARC Terms of Use box (the owner ticks it before the run: lesson 39); the Summary's button is "Save", and like every App content card Save it is yours (lesson 41). "Send changes for review" is the owner's real click; managed publishing is off by default, so approved means live (play.managedPublishing). - Guard: the plans above,
check(category warning, open App content answers),validate(category reminder, "production has 0 countries"),play status/play releasenotes;tests/console-play-plans.test.ts.
28. First App Store IAPs: apple iap-submit --with-version, not the console
- Symptom: the IAP page's "Add for Review" menu ignored scripted clicks, and the version page no longer has an "In-App Purchases and Subscriptions" section.
- Do:
storebound apple iap-submit --with-version --dry, then after the owner's yes (it submits)--with-version --yes: on a real first release it added the IAPs to the draft submission that already held version 1.0.0 and submitted them, first try. App Privacy: the owner's Publish opens "Publish Your App Privacy Responses?" (Cancel / Publish); afterwards "Published … ago by …" shows, whichconsole verify apple-app-privacyreads as the published signal. - Guard: appstore-first-release and store-iap-setup skills;
tests/console-play-plans.test.ts.
29. ITMS-91064: NSPrivacyTracking true with an empty NSPrivacyTrackingDomains
- Symptom: Apple marks the build Invalid Binary a minute after the upload: "ITMS-91064: Invalid tracking information - A PrivacyInfo.xcprivacy file contains invalid tracking information at the following path: "PrivacyInfo.xcprivacy". NSPrivacyTracking must be true if NSPrivacyTrackingDomains isn't empty. Keys and values in your app's privacy manifests must be valid."
- Cause: the app's own PrivacyInfo.xcprivacy said NSPrivacyTracking true with an empty NSPrivacyTrackingDomains because App Privacy says "used for tracking", but the tracking came from an ad SDK (mediation, attribution) whose own manifest declares it. Apple's text names the other direction (domains listed with false); both are refused.
- Do: in every manifest, NSPrivacyTracking true needs a non-empty NSPrivacyTrackingDomains (domains the code of
THAT bundle sends tracking data to), and a non-empty list needs true. When only an SDK tracks, the app's manifest
says false; App Privacy in App Store Connect still says tracking = yes and ATT stays. Set
appPrivacy.trackingDomainsonly for the app's own tracking domains. A bad SDK manifest: update the SDK. Rebuild and upload a new build number. - Guard:
check,scanandprivacy-manifestfail on either mismatch with the ITMS text;checkreports tracking declared by an SDK manifest as info and warns only when no manifest declares it;privacy-manifest --proposewrites true only withappPrivacy.trackingDomains;apple uploadchecks every manifest in the IPA (app root, Frameworks, bundles) before any network call,apple buildafter the export;tests/privacy-tracking.test.ts.
30. Resubmit after a rejection: reuse the UNRESOLVED_ISSUES submission
- Symptom: after an Invalid Binary rejection,
apple iap-submit --with-version --yescreated a new, empty draft and stopped with "the store says it already exists but does not list it yet (its lists lag ...)". The real answer was 409ITEM_PART_OF_ANOTHER_SUBMISSION: the version was still in the rejected submission. - Cause: a rejected submission stays open with UNRESOLVED_ISSUES and keeps its items (the REJECTED version, its IAPs); storebound only looked for READY_FOR_REVIEW drafts, and read the 409's "already added" as a duplicate.
- Also seen (same day): with build 2 VALID, APP_STORE_ELIGIBLE and attached, every
PATCH reviewSubmissions/{id} submitted: trueanswered 409 STATE_ERROR "Version is not ready to be submitted yet, please try again later" for 25 minutes, and the console's Resubmit and Add for Review buttons were disabled. Retrying never helps: the appStoreVersion item was still REJECTED.PATCH /v1/reviewSubmissionItems/{item id}with{"data":{"type":"reviewSubmissionItems","id":"<item id>","attributes":{"resolved":true}}}answered 200, the item went to READY_FOR_REVIEW, and the submit then passed (WAITING_FOR_REVIEW). The 7 IAP items were READY_FOR_REVIEW. An empty READY_FOR_REVIEW draft (0 items) cannot be canceled:canceled: trueanswers 409 "Resource is not in cancellable state". It is harmless; leave it (a later run reuses it). - Do: upload the fixed build,
storebound apple status(shows the submission, its items, which are REJECTED and not resolved, and the command), thenstorebound apple resubmit --build <n>(dry run, plan table with "mark resolved" rows) and after the owner's yes--yes: it attaches the build, marks the REJECTED version item resolved, then submits. REJECTED IAP / subscription items are marked resolved only with--resolve-iaps(after fixing what App Review asked for). Right after a build is attached, "not ready yet" with nothing left REJECTED is retried with backoff (up to 10 min) and a re-run continues where it stopped. Never offer a cancel command for an empty draft. - Guard:
chooseSubmission(cli/lib/review-submit.ts) reuses an open submission and never creates a second one;addItemturns the 409 into a message naming the other submission (never the lag message:isConflictexcludes it), a draft created by the same run is left as is (not cancellable) and the named one used;resolveRejected/toResolvemark REJECTED items in scope resolved before the submit;submitWithRetry(cli/apple/submission.ts) re-reads the items on "not ready", resolves those in scope and stops at once (naming the item and how to resolve it) when one out of scope is left, backing off only when none is;apple statuslabels an empty draft "empty draft (harmless; App Store Connect keeps it, not cancellable)" and treats the cancel 409 as harmless; the fake ASC blocks the submit while an item is REJECTED and refuses to cancel an empty draft;tests/resubmit.test.ts.
31. After a canceled submission: DEVELOPER_REJECTED versions go in as they are
- Symptom: the owner canceled a WAITING_FOR_REVIEW submission;
apple iap-submit --with-version --drythen skipped every IAP ("latest version is DEVELOPER_REJECTED: change it in App Store Connect (or --new-version)") and said "nothing to submit". - Cause: canceling leaves the app version DEVELOPER_REJECTED (then PREPARE_FOR_SUBMISSION) and every IAP version
DEVELOPER_REJECTED. Apple accepts the same IAP version again:
POST /v1/reviewSubmissionItems→ 201 READY_FOR_REVIEW, then the submit → 200 WAITING_FOR_REVIEW. storebound treated it like an approved product. - Do:
storebound apple iap-submit --with-version --dry: the plan lists them as "add (developer rejected, re-submitted as is)" into the open draft (an empty one is reused); then after the owner's yes--yes. Use--new-versiononly for APPROVED or REJECTED products whose metadata changed. - Guard:
planIapSubmission(cli/lib/review-submit.ts) takes DEVELOPER_REJECTED IAP, subscription and group versions; the fake ASC models the cancel and the re-add;tests/resubmit.test.ts.
32. play build: the AAB keeps the build file's versionCode
- Symptom:
play buildpassed versionCode 3, the AAB had versionCode 1 (the post-build check stopped it). The Capacitorapp/build.gradleread versionCode from an env var. - Cause: storebound passes
-PversionCode/-PversionName(andandroid.injected.version.*); a build file that does not read the project property ignores them.android.injected.version.codedoes NOT overridedefaultConfigforbundleRelease(checked with AGP 8.13). - Do: in
defaultConfig, Groovy:versionCode((project.findProperty('versionCode') ?: 1) as Integer)andversionName project.findProperty('versionName') ?: '1.0'; Kotlin:versionCode = (project.findProperty("versionCode") as String?)?.toInt() ?: 1andversionName = project.findProperty("versionName") as String? ?: "1.0"(your current values as defaults). NotversionCode (project.findProperty('versionCode') ?: 1) as Integer: Groovy reads it asversionCode(...) as Integerand Gradle fails with "Value is null". - Guard:
play build(also--dry) andcheckwarn before Gradle with the file, the line and the line to use; the post-build versionCode check is a hard stop;tests/build-sign.test.ts,tests/web-assets.test.ts.
33. Play "Do you want to restart your review?"
- Symptom: in Publishing overview, "Submit N changes for review" while a review is running opens "Do you want to restart your review?" with "Restart review".
- Cause: Play reviews one set of changes at a time; restarting sends the new changes and starts the running review over.
- Do: never press "Restart review" as an agent: tell the owner a review is running and what restarting means; the owner decides (Cancel keeps the running review).
- Guard: the
play-production-releaseconsole plan lists the dialog, "Restart review" is in neverPress;tests/console-play-plans.test.ts.
34. Desktop release: never run the x86_64 slice on the build Mac
- Symptom: while building the storebound desktop app, macOS tells the owner "App Update Required: this version of storebound includes a component that will not open in macOS 28".
- Cause: a check ran the universal app's Intel slice (
arch -x86_64 ... --self-test) on Apple silicon, which starts Rosetta; macOS warns about every app that does. - Do: check the x86_64 slice only by structure (
lipo -archs,file,codesign) and run self-tests natively (arm64). The same holds for any universal app or game you build: never launch the Intel slice to test it. - Guard:
tools/desktop-release.ts(archProblemsbefore signing: every Mach-O must contain arm64, no Intel-only file, no Info.plist that forces Intel;sliceProblems: every Mach-O in the bundle must carry arm64 and x86_64, nothing x86_64 is run);tests/desktop-release.test.ts.
35. Scan findings from a bare word, a module path or a config claim
- Symptom:
scanreports "A reward for rating or reviewing the app" onconst rate = F.genRate(lv, bonus.gen(r));; account findings citeimport './buildings/register';; 5.1.1(v) disappears onceaccounts.inAppis set although the app has no delete-account code. - Cause: the signals matched "rate" next to "bonus", and "/register'" anywhere on a line; the config's word was taken as proof.
- Do: read every finding's evidence before acting on it. A finding whose evidence is not real code is a scan bug:
report it (and accept it with
scan.ignoreand the owner's reason meanwhile). For 5.1.1(v), show the deletion code path or screen; a config line alone is a claim, not evidence. - Guard: the incentive signal needs a rate-us / review phrase next to a reward, a reward "for rating", or a reward
next to a system review prompt call; sign-in / sign-up / deletion signals skip import and require lines and accept
only absolute auth paths or URLs; config-only accounts read "from config (accounts.enabled)"; deletion claimed only
by the config stays a medium finding ("claimed by config, not found in code").
tests/game-report.test.ts.
36. Privacy answers derived from the wrong source
- Symptom:
privacy-manifest --proposeprints an empty NSPrivacyCollectedDataTypes whileappPrivacy.typeslists collected types; the App Privacy plan adds "Other Usage Data" you never listed; the Play plan answers "uses advertising ID: Yes" because Data safety declares a Device ID. - Cause: the proposal only kept the manifest's own entries; the App Privacy sheet mixed in types derived from dataSafety; the advertising ID answer read PSL_DEVICE_ID (a random install id is not the advertising ID).
- Do: treat
appPrivacy.typesas the App Privacy answer: a type it does not list is "not collected". Answer the advertising ID from the SDKs and the merged manifest (com.google.android.gms.permission.AD_ID), never from Data safety alone; setconsole.appContent.advertisingIdwhen the signals disagree. - Guard:
--proposebuilds the list fromappPrivacy.types(linked, tracking, purposes; NSPrivacyTracking true only with app-level domains, lesson 29) andcheckwarns on an empty list;console planscans the project (ad / attribution SDKs, AD_ID, support and AR code) and answers No with no signal, asks on a conflict.tests/game-report.test.ts.
37. scan.ignore that matches nothing; placeholder credentials
- Symptom: a
scan.ignoreentry does not accept the finding and nothing says why; commands treatYOUR_KEY_IDor/absolute/path/to/upload.p12in.storebound/credentials.jsonas a credential. - Cause:
matchis compared with the finding TITLE, not the evidence; the 0.7.1 credentials template held example-looking values. - Do:
scan.ignore: [{ rule, reason, match?, evidence?, store? }]:match= text in the finding title (one check of a rule with several),evidence= text in one evidence line as the report prints it (src/main.ts:12or a code fragment); both together narrow further. Never add an entry without the owner's reason. Fill credentials with real values or leave them empty. - Guard:
scan(notes) andcheck(warnings) name every entry that accepts no finding; the credentials template is empty strings; "your-…",<…>, "xxx", "/absolute/path/to/…" and "example" values count as missing everywhere (checkwarns,doctorlists them).tests/game-report.test.ts.
38. New App: Name and SKU empty after a value set
- Symptom: App Store Connect's New App dialog showed the Name and SKU, yet the dialog's state was empty ("").
- Cause: the dialog is React: a programmatic value set (
form_input,el.value = …) changes the input, not the component state; clicking the field and typing works (verified 2026-10-13). - Do: type, don't set value: click the field, type the text key by key, read it back.
- Guard: the
apple-app-recordplan (uiFacts, the fill step);console filltypes every text field (typeText);tests/browser/console-flow.browser.ts.
39. IARC page 1: Next does nothing until the Terms box is ticked
- Symptom: on the content rating questionnaire's page 1 Next did nothing, and the agent waited mid-run for the owner.
- Cause: the IARC Terms of Use checkbox gates Next, and it is an agreement only the owner ticks.
- Do: hand the owner the plan's
ownerBatchin ONE message before the run: it has "Tick the IARC Terms of Use box" with the exact place (Content ratings > Start questionnaire, page 1). Never tick it yourself; if it is still unticked, say so once and poll it, do not press Next again and again. - Guard:
ownerBatchinconsole plan play-content-rating/play-app-content(printed first);console fillwaits for the tick ("agreement needed on …");tests/console-play-plans.test.ts,tests/browser/console-flow.browser.ts.
40. Finding play.appId in a paginated app list
- Symptom: finding a new app's Play Console app id took several steps: 10 apps per page, the new app not on page 1.
- Cause: the Play API does not return the id; it is only in console URLs.
- Do: after Create app read
location.href(/app/<appId>/app-dashboard) and runstorebound console verify play-app-record --url "<url>"(dry diff), then--write(an upsert ofplay.appId). Missed it:storebound play app-id(search the package name in the list or walk the pages, pipe the captures in), then--write. - Guard:
playAppIdInText,findPlayAppId,upsertNestedString;tests/play-app-id.test.ts.
41. App content card Saves are the agent's
- Symptom: every Play App content card ended in an owner-only Save, so the owner came back once per card.
- Cause: a saved declaration was treated as public. It is not: Google gets it only with the next "Send changes for review" in Publishing overview (verified 2026-10-13).
- Do: press each card's Save yourself (privacy policy, ads, app access, advertising ID, government, financial,
health, target audience, the content rating questionnaire and Summary, data safety). The owner keeps: the IARC
Terms box (before), typing the demo account only when it is not in
.storebound/credentials.json(otherwise you type it, never printed), and "Send changes for review" / Submit / Publish / "Restart review" (after).storebound console plan play-app-content --allwalks every card and ends with that one owner list. - Guard:
declarationSaveAllowed,publishingControl,flowAllowed(cli/lib/console-forms.ts), the plans'buttons.final"Send changes for review",console fill's declaration Save;tests/console-play-plans.test.ts.
42. A filter the spec declares, refused live (TestFlight distribute)
- Symptom:
apple testflight distribute --build 1 --group Team --yesprinted the build state, then failed:GET /v1/betaGroups?filter[app]=…&filter[builds]=… -> 400. Before the groups existed it saidunknown group "Team" (). - Cause: Apple's OpenAPI spec declares
filter[builds]onGET /v1/betaGroups, but App Store Connect answers 400; the spec has no GET of a build's groups. An internal group with all builds already has every build and refuses one added by hand. - Do: read a build's groups per group (
GET /v1/betaGroups/{id}/relationships/builds); skip internal all-builds groups ("already available"). A configured group missing on App Store Connect:storebound apple testflight groups syncfirst. Never use a query parameter that is not declared for the operation (storebound api list), and add one refused live toAPPLE_LIVE_REJECTED(cli/api/client.ts). - Guard:
tests/asc-query-guard.test.ts(every App Store Connect GET with a query incli/against the spec andAPPLE_LIVE_REJECTED), the fake App Store Connect's 400 for such parameters,unknownGroupMessage;tests/idempotency.test.ts(distribute),tests/testflight.test.ts.
43. Keyword field: 100 characters, not bytes
- Symptom:
check/listing check/check --asocalled live keyword fields too long ("105 bytes > 100") that App Store Connect accepted. - Cause: App Store Connect counts characters. Live READY_FOR_SALE versions (read 2026-10-13): Thai 95 characters / 265 bytes, Russian 91 / 169, Turkish 100 / 105, Polish 99 / 105. Entry 4 (bytes) was wrong.
- Do: count characters (
[...text].length); fill up to 100 characters in any script. - Guard: every keyword check counts characters (
cli/lib/copy.ts,aso.ts,listing-translate.ts,aso-intel.ts); tests with Thai, Turkish and Polish fields over 100 bytes.
44. "No 6.7-inch iPhone screenshots" on a live app with the 6.5-inch set
- Symptom:
validateblocked every locale of a live app: "en-US: no APP_IPHONE_67 screenshots". - Cause: App Store Connect requires the 6.9" set (APP_IPHONE_67) or, when it is missing, the 6.5" set (APP_IPHONE_65), and scales the smaller sizes from it.
- Do: treat the 6.5" set as meeting the requirement; adding a 6.9" set is a recommendation.
- Guard:
ACCEPTED_INSTEADincli/lib/validate-rules.ts(a warningscreenshots-size, no blocker).
45. Play graphics missing in extra languages
- Symptom:
validate --playlisted an icon and a feature graphic blocker for every non-default language. - Cause: Play shows the default language's graphics in a language that has none of its own.
- Do: only the default language's graphics are required; a language with none uses them (a note).
- Guard:
playRules(images-fallbacknote per type).
46. Play Developer Reporting API off
- Symptom:
apps list --store playand Play vitals failed with a 403 SERVICE_DISABLED. - Cause: the Reporting API is not enabled in the service account's Google Cloud project.
- Do: open the link the finding prints (
…/playdeveloperreporting.googleapis.com?project=<id>), press Enable, wait a few minutes. Meanwhileapps listshows the Play apps of this machine's folders and store-only entries. - Guard:
reportingEnableUrl,knownPlayPackages(cli/apps.ts),healthFindings.
47. Onboarding a live app left its products out ("no USD prices")
- Symptom: after
init+listing pullthe config had no products although the app sells some. - Cause: only texts were pulled, never prices; Apple's price schedule can have a base territory other than the USA.
- Do:
storebound onboard(then--write): products and subscriptions from both stores with usd = the US price, Apple's base territory as an override, one-store items marked withplatforms. Thenstorebound catalog diff(clean). Store prices are never changed; an item with no USD price anywhere is named with its base price. - Guard:
importCatalog(cli/lib/onboard.ts),tests/onboard.test.ts.
48. Contact e-mail, website and App Review contact asked of the owner
- Symptom: onboarding kept
support@example.comand asked for the website and review contact. - Cause: nothing read Play's app details or App Store Connect's review detail.
- Do:
storebound onboardreads them; template values are replaced, real ones kept; the review contact goes to.storebound/credentials.json(masked in output, the phone never printed). - Guard:
contactProposal,reviewContactOf,maskContact.
49. Sign-in for review defaulted to yes
- Symptom: every onboarded app got "sign-in required: add a demo account".
- Do: decide from Apple's review detail (
demoAccountRequired; a demo account there is imported, never printed) and from the code's auth signals; ask only when neither proves it. - Guard:
signInDecision.
50. Account deletion URL asked although the site has the page
- Do:
storebound onboardtries /delete-account, /account/delete, /delete, /account-deletion and the privacy policy's deletion links; not found is reported as the real gap it is (Play requires the page). Never invent one. - Guard:
findDeletionUrl.
51. No default credential profile except one named "default"
- Do:
storebound accounts default <profile>(--clear). Order: env > the project's named profile > the project's own keys > this default > the only profile. - Guard:
defaultProfile,tests/machine.test.ts.
52. A live app with no local project is missing from the desktop
- Do:
storebound apps add --store apple --app-id <id> --account <profile>; the desktop shows it read-only ("Store only": status, reviews, analytics).apps remove apple:<id>takes it out. - Guard:
storeAppsin~/.storebound/projects.json(cli/lib/registry.ts,registry.rs), the boot check.
53. apple upload passed a stale IPA as "already VALID"
- Do: upload the IPA
apple buildjust printed (build/ios/export/<version>-<build>/App.ipa), orapple upload --build N. An IPA whose build number is already on App Store Connect now fails with exit 1: the last build failed; runapple buildagain.--allow-existingonly for re-checking a build this command uploaded before (flows use it with--build ${BUILD}). - Guard: per-build export folders,
existingBuild/exportedIpas(cli/apple/upload.ts),exportIpa(cli/apple/build.ts),tests/apple-build-export.test.ts.
54. "exportArchive Copy failed": a Homebrew rsync on PATH
- Do: read the IDEDistribution log lines
apple buildprints. "rsync error: syntax or usage error" means a non-Apple rsync (Homebrew 3.x) is first on PATH; storebound runs xcodebuild with /usr/bin first,storebound doctornames the culprit. Never install orbrew linka global tool as a side effect on a shared machine. - Guard:
xcodePath,foreignRsync,distributionErrors(cli/lib/xcode.ts), doctor,publishSteps(tools/desktop-release.ts, rsync by absolute path).
55. The feature graphic cut the phone off at the bottom
- Do: nothing by default:
shots.feature.fit'contain' sizes the (rotated) phone so its outline stays inside the 1024 x 500 canvas with a 24 px margin.fit: 'bleed'keeps the larger, cropped phone on purpose;scale(0.5-1.5) resizes it.shots checkfails a feature graphic whose device leaves the canvas (measured from the page's layout at render time,devicesin report.json), and warns when a screenshot's front device runs off a side or the top. - Guard:
featureLayout,rotatedBox(cli/shots/lib.ts),deviceIssues(cli/shots/report.ts),tests/browser/feature-fit.browser.ts(geometry, both templates rendered at 1024 x 500, a bleeding fixture).
56. "Live on Play" while Google was still reviewing, and 1.0 / 1.0.0 shown as two versions
- Do: never call a Google Play production release live from the Developer API alone: a release reads
completed(userFraction 1) from the moment it is sent, also while Google reviews the first release.play statuschecks the public store page (no auth, 4 s timeout, cached 5 minutes) and decides, never "maybe": the page's "Version" (About this app, read from its AF_initDataCallback data) equals the release's versionName = "Live"; 404 = "In review" (with play.managedPublishing: "In review / ready to publish (managed publishing)"); an older version on the page = "In review (live: )"; "Varies with device" = live. Only a network failure gives "Couldn't check the store page", and then the last known page answer is used with its time. Match versions across the stores byversionKey(trailing .0 parts dropped: 1 == 1.0 == 1.0.0); show each store's own string. - Guard:
playPublicPage(cli/lib/play-public.ts),playReleases/versionKey/sameVersion(cli/lib/portfolio.ts),versionHistory(cli/lib/train.ts),tests/play-public-version-match.test.ts, the desktop boot check'srelease-statecases (page 404 and 200).
57. A raw Google JSON error in the window, and "Exit code 1" for a scan that found problems
- Do: never show an API error body to the owner. Parse Google errors into a typed finding (
parseGoogleError): SERVICE_DISABLED = the API is off in the service account's Cloud project (say which API and project, link the console page to turn it on), PERMISSION_DENIED = the service account has no access to that app (Play Console > Users and permissions), 401 / invalid_grant = the key is revoked or expired, 429 / RESOURCE_EXHAUSTED = quota. One project that blocks several apps is one notice, not one error per app. Keep the raw text in a collapsed Details and the log. Exit 1 ofscan/check/validate/healthwith the JSON result printed means "found problems": show the findings; a real failure shows the command's own error line, never a bare exit code. - Guard:
cli/lib/google-errors.ts,outcomeOf/errorLine(cli/lib/desktop-protocol.ts),tests/google-errors.test.ts, the desktop boot check'serrorscases (api-disabled, scan-findings, scan-failed).
58. Parallel Play reads deleted each other's edits, and a failed read said "not on Google Play yet"
- Do: Google Play keeps one open edit per app: a second
edits.insertfor the same package deletes the first, so a read in it answers 400 "This Edit has been deleted". Every edit (read or write) of a package takes the machine-wide lock~/.storebound/locks/play-edit-<package>.lock(short wait with a heartbeat log, 90 s timeout for reads, 10 min for writes; stale when its process is gone or untouched for a minute); other packages stay parallel. A read whose edit was deleted, committed elsewhere or expired is redone once with a new edit; a write is never redone that way (it keeps its own invalidation retry). A store that could not be read is "couldn't read Google Play" (unknown), never "not on Google Play yet"; its last good releases stay with "couldn't refresh". When the stores' current versions differ (normal), each lane shows its own ("4.1.2 · Live (4.1.9 not submitted)") under a neutral "Current" header; "Not on this store" only when the store has no record of the app. - Guard:
withMachineLock(cli/lib/lock.ts),read()/edit()(cli/play/client.ts),laneGap/storesDiffer/withLastGood(cli/lib/train.ts),tests/play-edit-lock.test.ts(a fake Play where a second insert invalidates the first), the desktop boot check's auto-read case (two Play reads of one app at once, Orbit Hop on different versions per store).
59. A live app showed missing art and render blockers although both stores had its screenshots
- Do: for an app that is already live, pull the art instead of asking for render sources:
storebound art pull --dry, then--write(onboardruns it too). The App Store CDN re-encodes: a downloaded screenshot never has the MD5 App Store Connect keeps for the uploaded file (sourceFileChecksum), so without a recordapple screenshotswould re-upload every set.store/renders/pulled.jsonkeeps each pulled file's sha256 with the store's own checksum, and the uploads compare an unchanged pulled file with that checksum (nothing sent). Play serves the original bytes at=s0(its sha256 equals the oneimages.listgives; checked on download). Never overwrite a local file that differs from the store without--force: it may be the owner's new render. - Guard:
cli/art.ts,cli/lib/art-pull.ts(decide,pulledChecksum,storeEmptySets), the pulled-file rules inscanRenders(cli/shots/report.ts) andcheck,tests/art-pull.test.ts(recorded ASC screenshot sets withtemplateUrland Playimages.list, the layout, the upsert and--force).
60. init + onboard on live apps left the template compliance answers in the config
- Do: treat every answer
storebound initwrote as unanswered. The game template declares approximate location, app interactions, diagnostics and device id as shared for advertising andPSL_ACM_NONE; pushing that from a live app is a false declaration. Templates list the areas inunanswered; configs from before it are recognised by the template values.check,validateandscanblock;play data-safety(also--dry),console plan/fill,apple review-detailsandapple appinforefuse. For a live app runstorebound onboard --declarations --write: name and copyright from the stores (a copyright with a private term fromSTOREBOUND_PRIVATE_TERMSis left out), the template answers out, the rows the code proves in (scan proposal, sign-up methods from auth code), the rest as one owner question list. Remove an area fromunansweredonly after the owner answered it. - Guard:
cli/lib/template-answers.ts,cli/lib/onboard-declarations.ts, thetemplate-answersscan rule,tests/template-answers.test.ts(init blocks, data-safety --dry refuses, onboard on auth code without an ad SDK).
61. prices ppp --json printed log lines on stdout before the JSON
- Do: with
--json, read stdout as one JSON document and stderr as the log. Every command run with--jsongoes throughwithJsonOutput(cli/lib/log.ts, wrapped in cli/index.ts): log / done / tables go to stderr,printJsonis the only writer to stdout, prompts go to stderr. Never print a result withconsole.log(JSON.stringify(...)). - Guard:
tests/json-stdout.test.tsruns every command whose module reads--jsonagainst the fake stores andJSON.parses its stdout, plus one real CLI run (prices ppp --offline --dry --json).
62. PPP prices cut Germany to EUR 3.59 for a USD 4.99 product (store price EUR 5.99)
- Do: trust
prices pppdefaults: only lower prices, only economies with a price level at most 50 % of the US, World Bank high-income economies and mainland China keep the store's own US-equivalent price, never above it; a top-10 revenue market dropping more than 20 % is warned about. The old math set a net price (base x PPP level x exchange rate) against VAT-inclusive store prices and cut every region. Now par = the store's own conversion (Play convertRegionPrices, Apple equalizations; offline base x fx x (1 + VAT), rounded up), share = clamp(level, 0.3, 1). Regions that keep their price are left out of the sheet (keptin--json), soprices importleaves the store's price there.--include-high-income,--max-level,--min-ratio/--max-ratio,--no-only-decreaseswiden it, only after the owner asks. - Guard:
shareOf,offlinePar,planRows,topMarketDrops(cli/lib/ppp.ts),tests/ppp.test.ts(Germany keeps EUR 5.99, India and Turkey drop, CN skipped on Play).
63. Creating new IAPs also changed live prices of existing ones (a terminal "y" covered them)
- Do: never rely on a confirmation for price changes. Creating products never changes an existing product's price
unless
--allow-price-changesis passed, TTY or not; the terminal question ofplay products,prices import,play subscriptionsandapple subscriptionscovers creates and other updates only. With the flag, price changes get their own question listing each old -> new (in a terminal even with--yes). Dry runs list them in a separate "price changes on existing products (held)" section.apple iapnever changes an existing schedule. - Guard:
allowPriceChanges/heldSection(cli/lib/price-gate.ts),tests/price-gate.test.ts(TTY and non-TTY: a "y" creates the new product and holds the live price; the flag's question names each change).
64. signing ensure made a manual App Store profile for an automatic-signing app
- Do: with
apple.signing.style: automatic(the default),apple signing ensureonly keeps the App ID and its capabilities in step. It never creates certificates or profiles: Xcode owns them (xcodebuild -allowProvisioningUpdateswith the API key) and renews its managed profile when the capabilities change. The dry run says so. Profiles storebound made that nothing uses ("storebound App Store ") are listed by name with the exactstorebound api apple profiles_deleteInstance --param id=<id> --yescommand; deleting them is the owner's call, never run by storebound. - Guard:
ensureScope,unusedStoreboundProfiles(cli/lib/signing.ts),tests/signing-capabilities.test.ts.
65. A capability add "succeeded" but the App ID kept only IN_APP_PURCHASE
- Do: never report a capability as added from the POST answer.
signing ensurereads the App ID back after any capability write (a few tries with growing waits, about 45 s at most) and fails with the API's answers when one is missing. The capability names were right (CapabilityType: PUSH_NOTIFICATIONS, GAME_CENTER); what an App ID shows and what a build may use are different things: a provisioning profile keeps the entitlements of the moment it was made, and an app whose project has no.entitlements(or one not set in CODE_SIGN_ENTITLEMENTS) ships without the capability whatever the App ID says.apple status,apple signing statusandvalidatecompare the App ID,apple.signing.capabilitiesand the project's entitlements and name what is missing where. - Guard:
verifyCapabilities,compareCapabilities,ENTITLEMENT_CAPABILITIES(cli/lib/signing.ts),checkCapabilitiesStuck(cli/apple/signing.ts),tests/signing-capabilities.test.ts(live-shape bundleIdCapabilities answers, lag, a write that never sticks).
66. storebound api apple ... --body f.json --yes only dry-ran from an agent
- Do: expected: writes through
storebound apiare sent only from an interactive terminal, by design (since pitfall 78 it is a loud refusal with exit 1, not a dry run). Use the dedicated command the message names (bundle id capabilities:apple signing ensure; IAPs:apple iap; listing:apple listing, ...), which runs anywhere with its own dry run and read-back. - Guard:
dedicatedCommand(cli/api/gate.ts),tests/signing-capabilities.test.ts.
67. "builds ship without push / Game Center" while the uploaded build had both
- Do: a checkout without
.entitlementssays nothing about the build: native changes may live on a release branch on purpose (OTA fingerprint on main). The comparison warns only about this checkout ("built from another branch?"), names the current branch and the local branches / worktrees that have an.entitlementsfile, and is never a blocker. The real answer is the built IPA: with one in build/ios/export (highest build number, newest file) storebound reads its entitlements (codesign -d --entitlements :-on macOS, else the Mach-O signature blob) and compares those, labelled "uploaded build N". The App Store Connect API does not expose a build's entitlements, so without a local IPA it says that instead of guessing. - Guard:
checkoutInfo,builtIpaEntitlements(cli/apple/signing.ts),machoEntitlements,compareCapabilities(cli/lib/signing.ts),tests/signing-capabilities.test.ts(entitlements only on another branch; IPAs with entitlements).
68. The owner's Chrome would not open: an orphaned headless storebound Chrome
- Symptom: clicking Chrome in the Dock does nothing (no window);
psshows a Chrome with--user-data-dir=…storebound-profile-…(or…/.storebound/console-profile/…,storebound-chrome-…),--remote-debugging-port=0 --headless=newand parent PID 1. - Cause: a Chrome storebound started for a console flow (here a test run) outlived its run. Started with a DevTools port, Chrome does not notice its parent dying; macOS LaunchServices then counts the windowless instance as the running "Google Chrome", so the Dock only activates it.
- Do: run any
storebound console …command (orbun test tests/chrome-guard.test.ts): every start ends such headless orphans and names orphaned windows. By hand:kill <pid>of the process without--type=. Never kill a Chrome whose parent is a running storebound (a run that waits for its window), or the owner's own Chrome. - Guard:
guardChrome/startWatchdog/reapStaleChromes(cli/console/chrome-guard.ts): exit, Ctrl+C, SIGTERM / SIGHUP and the parent's death (SIGKILL included) end the Chrome;console fillcloses it on errors too; headless runs on macOS prefer Chromium / Chrome for Testing (its own bundle id) over Google Chrome.app (persistentExecutable);tests/chrome-guard.test.ts.
69. bun add -d storebound broke a Next app's build
- Symptom:
next build(or vue-tsc, ts-loader,tsc --noEmit) fails on the scaffoldedstorebound.config.ts:Cannot find module 'storebound' or its corresponding type declarations. - Cause: storebound <= 0.8.5 exported only its TypeScript source (
cli/lib/config.ts) and shipped no.d.ts. Bun imports .ts, so everything storebound tested passed; TypeScript never takes a package's .ts source as its types. - Do: update to 0.8.6+ (declarations for ES module and CommonJS projects). Remove any tsconfig
excludeof storebound.config.ts that was added as a workaround. - Guard:
buildEntry/pruneDeclarations(tools/build-node.ts, run by prepack) andtests/package-types.test.ts: the template config type-checks against the packed entry in ES module and CommonJS projects under bundler, node16, nodenext and node10 with no @types/node and skipLibCheck off;import()andrequire()of the package load under Node.
70. The first in-app purchase was not in the version's review submission
- Symptom: the app version has been WAITING_FOR_REVIEW for days while its first in-app purchase (here a starter pack) still says READY_TO_SUBMIT; the review submission's items hold only the appStoreVersion.
- Cause: Apple reviews the first in-app purchase of each type (and the first subscription of a group) only together with an app version. A submission made without it (from the App Store Connect version page, or before the product was ready) leaves it out, and the version is approved without it: the product is not sellable. storebound <= 0.8.6 printed the IAP and the submission states separately and called READY_TO_SUBMIT "complete".
- Do: before submitting,
storebound validate(blockeriap-first) andstorebound release plan; submit withstorebound apple submit --build <n>, which adds every ready product to the same submission and refuses to leave a never-approved first product out. Already in review: withdraw the submission (storebound apple status --cancel-submission <id>) and resubmit with the product, or add it with the next version. - Guard:
firstProductGaps(cli/lib/review-submit.ts) read bycli/apple/first-products.ts:apple status(WARN andfirstProductGapsin --json),release statusand the desktop checks (appleFirstProductWarnings),validate/release plan(blocker), theapple submitplan (blocker);iap-reviewsays "ready, not submitted yet";tests/first-products.test.ts.
71. The owner was sent to a console page with plain directions
- Symptom: a message said "App Store Connect > App Information > Age Rating" or "go to Play Console" and the owner had to find the app, the page and the card by hand.
- Cause: storebound's findings, fix hints and errors named console pages as text paths (40+ in cli/), without the app's URL.
- Do: when you send the owner to a console page, give the absolute deep link (storebound prints it;
storebound console planhas start URLs), never just "go to App Store Connect". Set apple.appId, play.developerId and play.appId so links open the app's own page instead of the app list. - Guard:
cli/lib/console-links.ts(every page key, absolute URL with and without ids;urlin validate / scan / declarations / release plan JSON, an Open button in the desktop) andtests/console-links.test.ts, which fails on a console path in cli/ or templates/ without an https URL.
72. Play refused to send an edit for review: "set changesNotSentForReview to true"
- Symptom:
storebound play release <aab> ... --dry(or a real run) uploads and updates the tracks, then edits.validate or edits.commit answers 400 "Changes cannot be sent for review automatically. Please set the query parameter changesNotSentForReview to true." - Cause: after a rejection (here production 1.0.0 (3), Broken Functionality), with other changes pending in the Console or with managed publishing, Play no longer sends an API commit for review by itself. The edit is fine; only the automatic send is refused. storebound <= 0.8.8 stopped there.
- Do: nothing by hand: storebound commits the same edit again with changesNotSentForReview=true (only edits.commit binds it; validate answers 400 "Unknown name ... Cannot bind query parameter")
(
--not-sent-for-reviewsets it from the start; MCPnot_sent_for_review). A dry run says the real run will commit that way. Then the owner presses Send changes for review in Publishing overview (storebound prints the deep link): that button is the owner's, never press or automate it. The API has no read of "waiting for Send changes for review", so storebound records its commit in.storebound/play-review-pending.jsonandstorebound play statusshows "changes committed - Guard:
isReviewRefusedand the edit flow incli/play/client.ts/cli/play/review-pending.ts(only that 400; no other request is repeated, the same edit id),play status(text andreviewPendingin --json);tests/play-not-sent-for-review.test.ts(fake Play refuses validate / commit once).
73. Guest-play apps rejected as a login wall
- Symptom: Play rejects a game that has no sign-in under "Play Console Requirements: provide valid login
credentials". The config said
reviewAccess: { signInRequired: false }and Sign in details answered "All functionality in my app is available without any access restrictions". - Cause: the reviewer's screenshot was the first-run screen: an empty required player-name field, the Continue button disabled until a name is typed, and an "I Have a Code" link that opens a transfer / recovery code dialog. To a reviewer an empty field plus a disabled button plus a code entry looks like a sign-in, and "no restrictions" gave them nothing to follow.
- Do (console): write English guest steps into
reviewAccess.instructions(keepsignInRequired: false; at most 500 characters), e.g. "No account needed. Open the app, the player name is pre-filled: tap Continue. 'I Have a Code' is optional (moves progress from another device); skip it."storebound console plan play-sign-in-detailsthen answers "All or some functionality in my app is restricted" with one entry named "Guest play (no account needed)", Username and Password empty and the steps in "Any other information required to access your app". The agent presses Add instructions, fills the dialog and stops: the dialog's required box "Sign in details in this declaration provide full access to all the features and content within this app, including premium or paid content" is a declaration the owner ticks (agents never tick declarations). Then the agent presses Add and Save; Send changes for review stays the owner's. App Review gets the same steps in its notes. - Do (in the app): pre-fill a suggested name so Continue is enabled at once, and make the code entry clearly optional ("Moving from another device? I have a code"), never the first thing a new player must do.
- Guard:
storebound checkwarns "Play may reject as a login wall; add guest steps" for a Play app with a first-run gate in the code (name entry, code / transfer entry, account transfer UI:SIGNALS.firstRunGate) or in-app purchases and noreviewAccess.instructions, and when the steps are longer than 500 characters (cli/lib/profile.ts). The plan's guest path is incli/lib/console-play-plans.ts(signInDetails);tests/guest-login-wall.test.ts.
74. Remote config and remote paywalls read as hidden features (2.3.1)
- Symptom: App Review rejects or removes an app under Guideline 2.3.1 (hidden, dormant or undocumented features), or Play flags Deceptive behavior, after a feature or a hard paywall was switched on remotely, or the reviewer saw a different paywall than the one users get.
- Cause: remote config / feature flag SDKs (Firebase Remote Config, Statsig, LaunchDarkly, GrowthBook, Optimizely, PostHog feature flags, Unleash, ConfigCat, Amplitude Experiment, Split) and remote paywalls (RevenueCat Paywalls, Superwall, Adapty, Qonversion, Purchasely, Apphud) change what the app does after review, and the review notes said nothing. Paywall tests and designs are not features and are fine once App Review knows; a business-model change (soft -> hard paywall, freemium -> premium, features moved behind a subscription) or functionality enabled after review is a feature change.
- Do: this is the developer's call; storebound only warns. Suggest a line in
apple.info.reviewNotes(andreviewAccess.instructions, which also go to Play App access) that names the SDK and what changes remotely (paywall designs, prices shown, which flags), document every feature that can be switched on or off, and send a business-model change as a new review with a description that discloses what needs a paid subscription. Never write the note into the developer's config or code yourself. - Guard:
storebound scan(andvalidate, which runs it;checkprints it as a warning) reportsremote-featuresfor Apple (2.3.1) and Play (Deceptive behavior), medium, with the deep links, unless the review notes mention remote config, a feature flag, A/B, a paywall test, an experiment or "remotely" (remoteFeatureFindingsincli/lib/scan/rules.ts, the SDK tableREMOTE_SDKSincli/lib/scan/sdks.ts). A code obfuscator (DexGuard, iXGuard, a JavaScript obfuscator) is a lowobfuscationnote; ProGuard / R8 minify alone is never reported.tests/remote-features.test.ts.
75. iPhone Duo screenshots: in the console, not in the API
- Symptom: App Store Connect shows an "iPhone Duo" tab next to "iPhone with Dynamic Island (medium display)" and "iPad 13" display", but an upload for it through the API has no display type to use (a guessed one answers 400).
- Cause: the API's
ScreenshotDisplayType(4.5.1) has no Duo value yet; only the asset library enum listsIPHONE_DUO. Duo is optional now, reportedly required from April 2027 for iOS 27.1 SDK builds. - Do: render and check the sets (
apple.iphoneDuo: true), add them in the console's iPhone Duo tab (the skip message has the deep link), never guess an enum name. For Play point foldables to the tablet sets; there is no Duo slot. - Guard:
IPHONE_DUO_DISPLAYincli/shots/spec.ts(null = not in the API; regenerate withbun tools/gen-api.tsand fill in the exact names),skippedDuoSetsincli/apple/screenshots.ts, thescreenshots-duonote in check / validate.tests/ppo.test.ts.
76. Header and Search Results assets are not uploaded like screenshots
- Symptom: a header or search results image was "uploaded" but the version page shows no header, a run waited on an asset nobody had sent to review, or a re-run uploaded the same file again.
- Cause: these slots take assets from the app's Asset Library (
GET /v1/apps/{id}/assetLibrary), not a set of their own: a slot is anappAssetLibraryPlacementsrow (PRODUCT_PAGE_HEADER_ASSETorAPP_STORE_SEARCH_RESULTS_ASSET, the version localization). An asset is reviewed only once it is in a review submission (its own item,reviewSubmissionItemswithappAssetLibraryImage/appAssetLibraryVideo; the console's "Add for Review"); after processing it is "Prepare for Submission". The version page's slots offer only approved assets ("Browse approved assets", seen in a real release, 20 Oct 2026), so placing waits for Apple's approval. Library assets carry nosourceFileChecksum, a rejected asset stays rejected (uploading the same bytes again only adds a copy), and the search slot is the API'sAPP_STORE_SEARCH_RESULTS_ASSET(SEARCH_RESULTS_ADS_ASSETis the Apple Ads one). Seen live (19 Oct 2026): placements of a live version read back with stateACTIVE, which the 4.5.1 spec'sAppAssetLibraryPlacementStatedoes not list; never validate placement states against the spec enum. - Do:
storebound apple creative-assets: MD5 inreferenceName("storebound "), reuse (or unarchive) a library asset with the same MD5, never re-upload a rejected file or one that failed processing twice.--add-for-reviewputs the unreviewed assets into the draft submission (with the first version, or alone once the app has an approved version; Submit for Review stays the owner's). An asset that is not APPROVED is reported per locale and placement as "uploaded , waiting for Apple's approval; runstorebound apple creative-assetsagain later"; the run is an upsert, so a later run places it as soon as it is approved (an approved old asset stays shown meanwhile).--verifyandvalidate/ the report show the same waiting state. With no editable version the live version is used. Whether the API accepts a new placement on a live version is not documented: storebound places the new asset first and removes the old one only after Apple accepted it, and on a 4xx it says so, leaves the live page unchanged, gives the live version page's deep link (select it there, or create a version and re-run) and does not try the remaining live slots. - Guard:
cli/apple/creative-assets.ts(classify,pickAsset,waitingText,creativeWaiting,processingTiming),tests/creative-assets.test.ts(fake App Store Connect: upload, wait for approval, place on a re-run, still processing, rejected / archived files, add-for-review without submitting, verify read-only with the waiting state, a refused live placement, the validate finding).
77. What's New shared with Play rejected on the App Store (2.3.10)
- Symptom: a Mac App Store version was rejected under Guideline 2.3.10 (Accurate Metadata): its What's New came from release notes shared with Google Play and had "Google Play Games" and "Android" lines. storebound wrote it without a word (the old check was a warning in the editor only).
- Cause: one notes source for both stores, no per-store split, and no write that refused other platforms. Google Play's Metadata policy is the mirror case (iOS, iPhone, App Store, Game Center, TestFlight in Play text).
- Do: tag store-specific lines
[play]/[apple]in shared notes (whats-new skill);storebound checkmust show noother platformline. A functional mention (a "Continue with Google" / "Sign in with Apple" button) is the owner's call:listing.allowPlatformMentions: [{ store, text, reason }]. Never add an entry yourself. - Guard:
cli/lib/cross-store.ts(term lists, per locale / field / line, the allowlist,notesForStore). Refused before anything is sent, dry runs included:apple listing,play listing,play release/play upload(release notes),listing set,whats-new check/apply, the desktop editor andstorebound edit(appleLocaleIssues/playLangIssueserrors).check/validateblockers (other-platforms),scanrulesother-platforms(2.3.10) andplay-other-platforms(Play: Metadata).tests/cross-store.test.ts.
78. storebound api ... --yes piped or redirected became a silent dry run
- Symptom:
storebound api apple <write> --yes | tail(or> out.json) printed a dry run and exited 0; the owner thought it was sent. Happened twice. - Cause: writes need an interactive terminal; without one the gate fell back to a dry run.
- Do: run writes without a pipe or redirect in a terminal, or use the dedicated storebound command.
- Guard:
decideincli/api/gate.tsrefuses (NOT_A_TTY): exit 1, the message on stderr, nothing sent, the dedicated command named. Without--yesa write is still a plain dry run.tests/api.test.ts.
79. A macOS (tvOS, visionOS) version was invisible next to the iOS one
- Symptom:
apple resubmitsaid "nothing to resubmit" while a macOS version was REJECTED with an UNRESOLVED_ISSUES submission;apple listing --version 0.8.7refused because the iOS 0.8.7 was live, although the macOS 0.8.7 was the rejected one to fix;apple statuslisted versions without saying which platform. - Cause: one app record can have a version per platform, each with its own state and its own review submission
(
reviewSubmissionscarry aplatform). Lookups filtered onIOSonly, or did not filter at all and mixed them. - Do: every version and review submission lookup filters by platform (
filter[platform]). Commands that need one version (status --wait-review, listing, listing pull, resubmit, submit, release, screenshots) take--platform ios|macos|tvos|visionos; without it they use the platform that has the version they need, and refuse naming both when several do. Status, validate, release status, the report and the desktop show the platform next to every version; validate and release status warn about another platform's editable or rejected version. - Guard:
cli/apple/platform.ts(pickPlatform,parsePlatform),tests/apple-platforms.test.ts(fake App Store Connect: iOS 0.8.7 live, macOS 0.8.7 rejected in UNRESOLVED_ISSUES).
80. Screenshot text and logo hugging the edges; a Mac app with no screenshots; sheet.png timing out
- Symptom: a game's hero logo and titles touched the left / right / top edge of the screenshots (the stores crop
and round the corners, it reads cramped); a game with a macOS version had no Mac screenshots (storebound had no Mac
size);
shots renderstopped with "sheet.png ... Timeout 30000ms" on a project with 9 sizes. - Cause: the templates padded the text by 5 % and drew the logo 86 % wide with a negative top margin, nothing measured it; the size table only knew iPhone, iPad and Play; the contact sheet used the same flat 30 s wait as one screenshot while decoding every image of the language.
- Do: keep the 7 % / 5 % safe margins (
shots checkcodemarginfails anything under 6 % / 4 %, measured from the page's layout boxes in report.json); setapple.mac: truefor a Mac app (macsize, 16:10,APP_DESKTOP, uploaded withapple screenshots --platform macos) and capture landscape frames; the sheet's wait now scales with images, sizes and load and logs a heartbeat (or use--no-sheet). - Guard:
marginViolations/SAFE_MARGINandrenderDimsincli/shots/lib.ts,marginIssuesincli/shots/report.ts,IMAGE_SPECS.macandmacNoteincli/shots/spec.ts,MAC_DISPLAYincli/apple/screenshots.ts,sheetTimeoutMs;tests/mac-landscape.test.ts,tests/browser/landscape.browser.ts, the macOS case intests/idempotency.test.ts.
81. storebound apps folders add listed the folders and added nothing
- Symptom:
storebound apps folders add(or with"$PWD") in a freshly init-ed folder printed the folder list, no error, and ~/.storebound/projects.json stayed without the folder. - Cause: only
apps addadded;apps foldersignored every word after it and listed. - Do:
apps folders add [<folder>]is now the same asapps add [<folder>](bare = the current folder). A folder whose config still has template values is added with a note; a folder without a config is refused with the reason and the fix (storebound init). Unknown words afterapps foldersare an error, never a silent list. - Guard:
foldersandtemplateNoteincli/apps.ts;tests/store-minimums.test.ts(apps folders add).
82. The MCP server failed to connect: spawn ENOENT, CONNECTION_CLOSED
- Symptom: the client showed
storebound (ENOENT) posix_spawn '<home>/…/bin/storebound'or "CONNECTION_CLOSED: Connection closed". - Cause:
.mcp.jsonspawned${CLAUDE_PLUGIN_ROOT}/bin/storebounddirectly; when that folder is gone (an old plugin cache after an update) or the variable is unset (the repo opened as a project), the spawn fails before any code runs. Under npx, the self-update could also start a second npx download before the handshake. - Do:
.mcp.jsonrunssh -c: the plugin's launcher if it exists, elsestoreboundon PATH, elsenpx -y storebound@latest mcp.mcpandserveskip the self-update. Nothing may print on stdout before the JSON-RPC answer (logs go to stderr). - Guard:
MCP_LAUNCHintools/gen-adapters.ts,cli/index.ts;tests/mcp-spawn.test.tsspawns the CLI, the launcher and the.mcp.jsoncommand with a missing plugin root and completesinitializewithin 10 s.
83. Uploads below the store's build minimum (Play target API level, App Store Xcode / SDK)
- Symptom: Google Play refuses a bundle with targetSdkVersion 35 (API 36+ for new apps and updates since 2026-08-31); App Store Connect refuses an IPA built with an Xcode / SDK older than the minimum (Xcode 26 / the 26 SDKs since 2026-04-28).
- Do: set
targetSdkVersion(andcompileSdkVersion) in android/variables.gradle (Capacitor), the app module's build.gradle(.kts), Flutter's android/app/build.gradle or Unity's Target API Level; build iOS with the current Xcode.scan/validate(rulesplay-target-sdk,apple-min-sdkfrom the newest IPA in build/ios/export) andplay upload/play release/apple uploadblock before any network call, naming the requirement, date and fix. - Guard: the dated tables
PLAY_TARGET_SDK_TABLEandAPPLE_MIN_SDK_TABLEincli/lib/store-minimums.ts(a new year is one row);tests/store-minimums.test.ts.
84. Thin app-style headlines, white backgrounds from a string palette.bg
- Symptom: a variable Google title family (a 200..800 axis) was downloaded as its default 400 instance, so the
750 headlines rendered thin (font-synthesis is off); a custom palette with
bg: '#15171C'or a gradient string rendered a white background, and whiteinkheadlines vanished. - Do: run
storebound shots fontsagain (it fetches the weights the templates use: full axis, used range, or static weights) andshots render. Usebgas stops, one colour or a CSS gradient string; for a photo usepalette.image/shots.feature.backgroundwith an overlay. Read thefont-weightandcontrastwarnings ofshots checkbefore uploading. - Guard:
cli/shots/weights.ts(templateWeights, fetchFaces, weightIssues),cli/shots/palette.ts(bgIssue in config validation, bgCss, contrastIssues, coverCrop); report.jsonweights/contrast;tests/shots-look.test.ts,tests/browser/palette-bg.browser.ts.