storebound/docs/pitfalls.md

59 KiB

Pitfalls: lessons from real use

A changelog of mistakes found while shipping real apps and games with storebound, newest first. Each entry says what went wrong, why, and what now guards it. The agent-facing short version is skills/store-pitfalls/SKILL.md; keep the two in step.

Rule (CONTRIBUTING.md): every bug found in real use adds an entry here and, where possible, a check or a test that fails without the fix.

# date lesson guard
96 2026-10 A pipeline could not ship a production update of a live app: production was treated as the owner's for every release, though the owner's rule is updates end to end and only the first-ever release gated. cli/lib/release-kind.ts reads update vs first from the store; --yes ships an update (Play play release --production / play upload --track production, App Store apple submit / apple release), a first release with --yes is exit 3 OWNER_STEP (--first-release or bypass approve it), a re-run is exit 7; tests/update-gate.test.ts
95 2026-10 The Google service-account keys moved to the owner's credential broker, but storebound still wanted a local PLAY_SERVICE_ACCOUNT, and a Play write with a missing permission ran its edit before Google's bare 403. cli/lib/broker.ts (broker.json, key file mode 600, token cache and single flight, grants, playPreflight before Play write commands), playToken in cli/lib/google-auth.ts (broker first, local key fallback when it does not answer), cli/lib/google-access.ts (403 SERVICE_DISABLED / PERMISSION_DENIED -> exact API or grant with its link, never retried), validate blockers; tests/credential-broker.test.ts against a local fake broker. Skill pitfall 122, docs/credential-broker.md.
94 2026-10 An app with managed publishing was approved but never went live: play status said "In review / ready to publish" with no next step, and the Publishing overview "Publish N changes" button (production + Data safety) was easy to miss or to confuse with Discard changes. console plan play-publish-changes (final "Publish N changes" is the owner's, Discard changes / Stop managed publishing never-press), console verify play-publish-changes (API), the hint in play status, status and the desktop Releases tab (cli/lib/play-publish.ts, tests/console-play-plans.test.ts).
93 2026-10 A git-ignored, never-captured frames folder rendered store images with no app or game in them; render only logged 404s and shots check said 0 errors. shots render refuses to start while a scene frame is missing (framesForRender in cli/shots/lib.ts) and fails when any load failed; report.json missing is a frame-missing error in shots check / validate (cli/shots/report.ts, tests/shots-missing-frames.test.ts).
90 2026-10 apple submit --build N --version X --yes after apple submit --no-submit --yes (the documented two-step flow) printed the plan (every item "in it already", then "submit PATCH submitted: true"), logged "not added ...: already in the draft submission" and ended with "nothing to submit" without the PATCH: the submit only ran for new items or an UNRESOLVED_ISSUES submission. willSubmit (cli/lib/review-submit.ts) also submits a READY_FOR_REVIEW draft that already holds the named app version (iap-submit: one of the asked-for items), with the same "not ready yet" retry. tests/resubmit.test.ts ("the second step submits the prepared draft").
85 2026-10 shots record --source web filmed at the page's CSS size: Chromium's screencast ignores the deviceScaleFactor (maxWidth only shrinks), so a 1080x1920 preview of a 430x932 @2x page was a 2.5x upscale. Page.captureScreenshot with clip.scale = dpr (long side capped at 1920), 3 requests in flight, timestamped frames joined by ffmpeg; a warning when the first frame is smaller than asked or the rate is under 20 frames/s. recordSize / captureShortfall in cli/shots/devices.ts, tests/video.test.ts.
80 2026-10 A game's hero logo and titles sat against the screenshot edges, a game with a macOS version had no Mac screenshots, and the contact sheet timed out at a flat 30 s with 9 sizes. Templates keep 7 % / 5 % safe margins and shots check fails text or logo boxes under 6 % / 4 % (marginViolations, report.json boxes); apple.mac adds the mac size (APP_DESKTOP, macOS version); sheetTimeoutMs scales with images, sizes and load. tests/mac-landscape.test.ts, tests/browser/landscape.browser.ts.
78 2026-10 storebound api <write> --yes with stdout piped or redirected printed a dry run and exited 0 (twice); the owner took it as sent. decide (cli/api/gate.ts) refuses with NOT_A_TTY: exit 1, "refused: writes through storebound api need an interactive terminal ... Nothing was sent." on stderr, the dedicated command named; without --yes still a dry run. tests/api.test.ts.
77 2026-10 A Mac App Store version was rejected under Guideline 2.3.10: its What's New came from release notes shared with Google Play ("Google Play Games", "Android"); storebound wrote it without a word. Play's Metadata policy is the mirror case. cli/lib/cross-store.ts: other platforms per locale, field and line (App Store text: Android, Google Play (Games), Play Store, Galaxy Store, Amazon Appstore, Steam, Microsoft Store, AppGallery, localized forms; Play text: iOS, iPhone, iPad, App Store, Apple Arcade, Game Center, TestFlight). Every text write refuses (apple / play listing, play release / upload notes, listing set, whats-new check / apply, the desktop editor and storebound edit); check / validate blockers; scan other-platforms (2.3.10) and play-other-platforms (Play: Metadata). listing.allowPlatformMentions for the owner's functional mentions; whats-new splits [apple] / [play] lines. tests/cross-store.test.ts.
74 2026-10 Apple sharpened Guideline 2.3.1 (hidden, dormant or undocumented features; obfuscation that hides features is grounds for removal). Apps with remote config / feature flags or remote paywalls whose review notes said nothing risk a rejection or removal when a feature or a hard paywall is switched on after review; Play's Deceptive behavior policy covers the same. Paywall tests and designs are fine once review knows; a business-model change needs a new review and a description update. remoteFeatureFindings (cli/lib/scan/rules.ts, SDK table REMOTE_SDKS in cli/lib/scan/sdks.ts): scan / validate report remote-features for Apple (2.3.1) and Play (Deceptive behavior), medium, unless the review notes (apple.info.reviewNotes, reviewAccess.instructions, accounts.inApp) mention remote config, flags, A/B, a paywall test, an experiment or "remotely"; check prints it as a warning. Warn only: storebound never writes the note; the templates carry a commented example line. Code obfuscators are a low note, minify alone nothing. tests/remote-features.test.ts.
73 2026-10 A game with guest play (no sign-in; reviewAccess.signInRequired: false, Play Sign in details "No restrictions") was rejected by Play: "Play Console Requirements: provide valid login credentials". The first screen had an empty player-name field with Continue disabled until a name was typed and an "I Have a Code" link to a transfer / recovery code dialog; the reviewer read it as a login wall. profileIssues (cli/lib/profile.ts, called by check with SIGNALS.firstRunGate hits from cli/lib/scan/rules.ts): warns "Play may reject as a login wall; add guest steps" for a Play app with a first-run gate or IAP and no reviewAccess.instructions, and when the steps exceed 500 characters. The play-sign-in-details plan (cli/lib/console-play-plans.ts): with guest steps "restricted" and one entry "Guest play (no account needed)" without credentials, the steps in "Any other information", the dialog's required full-access box as an owner step; Add and Save the agent's. tests/guest-login-wall.test.ts.
72 2026-10 A game: after Play production 1.0.0 (3) was rejected (Broken Functionality), the fix 1.0.1 (5) went through upload and track updates, then edits.validate answered 400 "Changes cannot be sent for review automatically. Please set the query parameter changesNotSentForReview to true". Play refuses the automatic send after a rejection, with other pending changes or managed publishing. cli/play/client.ts (edit flow) with cli/play/review-pending.ts: on exactly that 400 the same edit is validated and committed again with changesNotSentForReview=true (a dry run validates that way and says so), the Publishing overview deep link is printed (the owner's button), the commit is recorded in .storebound/ and play status shows it; --not-sent-for-review on play release / upload / rollout / listing / images (MCP not_sent_for_review). tests/play-not-sent-for-review.test.ts. Only edits.commit takes changesNotSentForReview: validate answers 400 "Unknown name ... Cannot bind query parameter" (the fake store does too).
71 2026-10 Messages sent the owner to console pages as plain text paths ("App Store Connect > App Information > Age Rating", "Play Console > App content") without a link; the owner had to find the app and the page by hand. cli/lib/console-links.ts: every message carries the absolute deep link (app list / developer chooser when ids are unknown), url in finding JSON, an Open button in the desktop; tests/console-links.test.ts fails on a console path in cli/ or templates/ without an https URL; agent rule 11 in AGENTS.md.
70 2026-10 A game: the app's first in-app purchase (a starter pack) was not in the 1.0.0 review submission (its items held only the appStoreVersion); it stayed READY_TO_SUBMIT and was noticed after two days in WAITING_FOR_REVIEW. Apple reviews the first product of a type only with an app version; approved without it, it is not sellable. firstProductGaps (cli/lib/review-submit.ts, read by cli/apple/first-products.ts): WARN in apple status (text and --json), release status and the desktop checks; blocker in validate / release plan and the apple submit plan; iap-review says "ready, not submitted yet". tests/first-products.test.ts.
69 2026-10 A web app: bun add -d storebound plus the scaffolded storebound.config.ts broke next build: the package exported only its TypeScript source and shipped no declarations; Bun imports .ts, so every storebound test passed, but tsc never takes a package's .ts source as its types. The package ships declarations for ES module (dist/types) and CommonJS projects (dist/types-cjs) and a Node entry (dist/config.js); Bun keeps the source (buildEntry, pruneDeclarations in tools/build-node.ts, run by prepack). tests/package-types.test.ts type-checks the template against the packed entry under bundler / node16 / nodenext / node10 in both project types with no @types/node and skipLibCheck off, and loads the package with import() and require() under Node. check fails on the template's own placeholders (Example Studio, bare example.com).
68 2026-10 A headless Chrome started by storebound's console automation (a test run's --user-data-dir=$TMPDIR/storebound-profile-apple-…, --remote-debugging-port=0 --headless=new) outlived its parent for ~20 hours as an orphan (parent PID 1). macOS LaunchServices took it for the running "Google Chrome", so the Dock's Chrome only activated the windowless instance and the owner's own Chrome would not open. A Chrome started with a DevTools port does not notice its parent dying. cli/console/chrome-guard.ts: every Chrome storebound starts itself is ended on process exit, closed gracefully on SIGINT / SIGTERM / SIGHUP, and ended by a detached watchdog when the parent dies (SIGKILL included; a reused pid is never killed); every start first ends headless orphans on storebound's profiles and names orphaned windows; headless runs on macOS prefer Chromium / Chrome for Testing over Google Chrome.app; console fill closes the window on an error too (tests/chrome-guard.test.ts).
67 2026-10 A checkout without .entitlements was reported as "builds ship without push / Game Center" while the uploaded build had both (native changes lived on a release branch). The comparison warns only about this checkout ("built from another branch?") and names the branches / worktrees that have an .entitlements file; with a built IPA it reads that build's real entitlements ("uploaded build N"); without one it says the App Store Connect API does not expose them. checkoutInfo, builtIpaEntitlements (cli/apple/signing.ts), machoEntitlements, compareCapabilities (cli/lib/signing.ts), tests/signing-capabilities.test.ts.
66 2026-10 storebound api apple ... --body f.json --yes only dry-ran from an agent. By design writes through storebound api need an interactive terminal; the message names the dedicated command (apple signing ensure, apple iap, apple listing, ...), which runs anywhere with its own dry run and read-back. dedicatedCommand (cli/api/gate.ts), tests/signing-capabilities.test.ts.
65 2026-10 A capability add "succeeded" from the POST answer, but the App ID kept only IN_APP_PURCHASE. signing ensure reads the App ID back after every capability write (retries, about 45 s at most) and fails with the API's answers; apple status, signing status and validate compare the App ID, apple.signing.capabilities and the project's entitlements. verifyCapabilities, compareCapabilities (cli/lib/signing.ts), checkCapabilitiesStuck (cli/apple/signing.ts), tests/signing-capabilities.test.ts.
64 2026-10 apple signing ensure made a manual App Store profile for an app with automatic signing. With signing.style: automatic it only keeps the App ID's capabilities in step (Xcode owns certificates and profiles; the dry run says so); unused storebound profiles are listed with the exact delete command, never run. ensureScope, unusedStoreboundProfiles (cli/lib/signing.ts), tests/signing-capabilities.test.ts.
63 2026-10 STAR DASH: re-running play products / prices import to create 8 new IAPs also changed the live prices of existing products; --only was needed. In a terminal (no --yes) the one confirmation covered price changes too. Price changes of existing products need --allow-price-changes, TTY or not; the terminal question covers creates and other updates only; with the flag a second question lists each old -> new (asked in a terminal even with --yes); dry runs show a "price changes on existing products (held)" section (allowPriceChanges, heldSection in cli/lib/price-gate.ts; play products, prices import, play subscriptions, apple subscriptions). tests/price-gate.test.ts (TTY and non-TTY).
62 2026-10 STAR DASH: prices ppp cut high-income markets (Germany EUR 3.59 instead of 5.99 for USD 4.99; UK, France, Japan cut too): it set base x PPP level x exchange rate, a net price, against VAT-inclusive store prices, and cut every region. Par = the store's own conversion of the US price (Play convertRegionPrices, Apple equalizations; offline base x fx x (1 + VAT) rounded up); share = clamp(PPP level, 0.3, 1); defaults: only decreases, PPP only where the level is <= 0.5, World Bank high-income economies and CN at par, Play skips CN, top-10 revenue market drops > 20 % warned (from .storebound/reports/ sales reports, else a static list); unchanged regions are left out of the sheet (shareOf, offlinePar, planRows, topMarketDrops, topMarketsFromReports in cli/lib/ppp.ts). tests/ppp.test.ts.
61 2026-10 STAR DASH: prices ppp --json printed log lines on stdout before the JSON, so parsing stdout failed. withJsonOutput (cli/lib/log.ts) wraps every --json run in cli/index.ts: console.log / info go to stderr, printJson writes stdout, prompts use promptOut(); commands that printed JSON with console.log use printJson. tests/json-stdout.test.ts runs every command that reads --json against the fake stores and parses stdout.
55 2026-10 The Play feature graphic (1024 x 500) cut the phone off at the bottom: the game template's phone was 232 px wide at top 36 and rotated 7 deg, so its outline spanned about y 24..552 and the game's bottom bar was lost; the app template bled too, and shots.feature only took frame + palette. Nothing measured the device against the canvas. featureLayout / rotatedBox (cli/shots/lib.ts) size the device so its rotated box fits the canvas with a 24 px margin (shots.feature.fit 'contain', the default; 'bleed' keeps the crop on purpose; scale 0.5-1.5), both templates use it. The templates record each device's measured layout box (devices in report.json) and shots check fails a feature graphic whose device leaves the canvas unless fit is 'bleed', and warns for a screenshot's front device off a side or the top (deviceIssues, cli/shots/report.ts). tests/browser/feature-fit.browser.ts.
54 2026-10 The second apple build of a Capacitor game archived fine, then xcodebuild -exportArchive failed with "exportArchive Copy failed" (exit 70). The IDEDistribution log had the real cause: rsync error: syntax or usage error (code 1) in IDEDistributionCreateIPAStep, because a Homebrew rsync 3.x, installed and linked that day by another session's release tool, came first on PATH. A tool changed a global binary on a shared machine as a side effect. apple build runs xcodebuild with Xcode's and Apple's tool folders first on PATH (xcodePath) and prints a note when a non-Apple rsync is first (foreignRsync); an export failure shows the error lines of the newest IDEDistribution*.log (distributionErrors, cli/lib/xcode.ts, cli/apple/build.ts). storebound doctor flags a non-Apple rsync first on PATH. tools/desktop-release.ts publish calls rsync by absolute path (/usr/bin/rsync without --chmod, modes set on the server; else the Homebrew keg), never from PATH (publishSteps). Rule: tools never install or relink global binaries as a side effect. tests/apple-build-export.test.ts.
53 2026-10 After that failed export, apple upload build/ios/export/App.ipa --yes uploaded nothing: it read the stale IPA of build 1, found build 1 already VALID on App Store Connect and exited 0 ("build 1 is VALID"), so build && upload looked green. The export also wrote into the same folder every time. apple build exports each build into its own folder build/ios/export/<version>-<build>/ (removed first) and exits non-zero on any export failure or a missing IPA (exportIpa). apple upload fails with exit 1 when the IPA's build number is already on App Store Connect ("this ipa is build 1, which is already on App Store Connect (VALID): did the last build fail?"); only --allow-existing re-checks it. --build N picks / requires that build's IPA; the release and beta flows use apple upload --no-wait --build ${BUILD} --allow-existing, so a resumed flow still passes and a stale IPA never does (existingBuild, exportedIpas, cli/apple/upload.ts; tests/apple-build-export.test.ts).
52 2026-10 An iOS app that is live on the App Store but whose source is not on this machine could not appear in the desktop: the machine's app list (~/.storebound/projects.json) took only folders. storebound apps add --store apple|play --app-id <id> --account <profile> writes a store-only entry (storeApps, no path; apps folders shows "no local project", apps remove apple:<id>); the desktop adds it to the app list of the account with that profile (registry::with_store_only, store_apps_cached / store_apps_refresh), shown read-only as "Store only"; writing the folder list keeps the entries. tests/onboard.test.ts, src-tauri/tests/machine.rs, the boot check's shared-profiles case (zero uncaught errors).
51 2026-10 With several credential profiles the only way to have a machine default was naming one default. storebound accounts default <profile> (and --clear) writes the pointer file <credentials>/default-profile; accounts list shows the default; order: env > a profile the project names > the project's own keys > the pointer > the only profile (or default) (defaultProfile, setDefaultPointer in cli/lib/credentials.ts; tests/machine.test.ts).
50 2026-10 Onboarding an app with sign-in left "account deletion URL" as an owner question although the app's own website had the page. storebound onboard looks on the app's site before asking: /delete-account, /account/delete, /delete, /account-deletion on the website and the privacy policy's host, then links in the privacy policy that mention delete / deletion, then a deletion section of the privacy page; a page counts only when it answers 200 and speaks of deleting. Not found stays a finding ("a real gap, Play requires it"); never invented (findDeletionUrl in cli/lib/onboard.ts; tests/onboard.test.ts).
49 2026-10 "Sign-in required for review" defaulted to yes during onboarding, so every app got a demo-account question. signInDecision: Apple's live review detail (demoAccountRequired) wins, else no auth code in the project (scan's auth signals, import lines ignored) means no; only when neither proves it is it an owner question. A demo account set on App Store Connect goes into .storebound/credentials.json reviewDemoAccount, never printed (cli/lib/onboard.ts, cli/onboard.ts; tests/onboard.test.ts).
48 2026-10 Onboarding left support@example.com and the template URLs in the config and asked the owner for the contact e-mail, website and App Review contact, all of which the stores have. storebound onboard fills app.contactEmail / website / supportUrl / privacyPolicyUrl / marketingUrl from Play's app details and App Store Connect where the config holds a template value (a real value that differs is reported, kept); the App Review contact goes into .storebound/credentials.json reviewContact, masked in every line (phone: last 2 digits) (contactProposal, maskContact; tests/onboard.test.ts).
47 2026-10 Setting storebound up on live apps left their products out of the config "because there were no USD prices": the import read only texts (catalog pull), never prices. storebound onboard --write imports the live one-time products and subscriptions of both stores: ids, type, listings per language, usd = the US price (else Play's own USD price for new regions), Apple's price schedule base territory as an override when it is not the USA, an item on one store only kept with platforms; catalog diff comes out clean; store prices are never changed, an item with no USD price anywhere is skipped with the reason (importCatalog; tests/onboard.test.ts runs catalog diff on the written config).
46 2026-10 apps list --store play and Play vitals failed outright when the Play Developer Reporting API was off in the service account's Cloud project, with a generic "enable it" hint. The finding names the project and the exact link (https://console.cloud.google.com/apis/library/playdeveloperreporting.googleapis.com?project=<id>, the id from Google's 403 or the service account e-mail: reportingEnableUrl); apps list no longer fails: it lists the Play apps of this machine's app folders and store-only entries instead, marked as not read from Play (knownPlayPackages, localPlayApps); healthFindings uses the same link. tests/onboard.test.ts, tests/store-real-shapes.test.ts.
45 2026-10 validate reported 34 blockers for a Play app whose 17 extra languages have no icon or feature graphic of their own; Play shows the default language's graphics there. playRules: a non-default language with none of its own uses the default language's graphics (one note per image type); only the default language's missing graphics block (cli/lib/validate-rules.ts; tests/wave1.test.ts).
44 2026-10 "en-US: no APP_IPHONE_67 screenshots" blocked live apps that have the 6.5" set: App Store Connect requires the 6.9" set or, without it, the 6.5" set (and scales down from it). ACCEPTED_INSTEAD in cli/lib/validate-rules.ts: the 6.5" set satisfies the 6.9" requirement (and either 13" iPad type the other); then a recommendation, not a blocker (tests/wave1.test.ts, a live-shape set list with only APP_IPHONE_65).
43 2026-10 The App Store keyword field is limited to 100 characters, not UTF-8 bytes (entry 4 was wrong): live versions READY_FOR_SALE read on 2026-10-13 hold Thai 95 characters / 265 bytes, Russian 91 / 169, Japanese 49 / 123, Turkish 100 / 105, Polish 99 / 105, and App Store Connect accepted them. check (cli/lib/copy.ts), check --aso (auditAppleKeywords), listing check (checkFields), listing translate (normalizeKeywords) and aso suggest (fitKeywordField) count characters; tests/listing-translate.test.ts, tests/wave1.test.ts, tests/aso-plus.test.ts with Thai, Turkish and Polish fields over 100 bytes.
42 2026-10 apple testflight distribute --build 1 --group Team --yes failed with 400 on GET /v1/betaGroups?filter[builds]=: Apple's spec declares the filter, App Store Connect refuses it, and the spec has no GET of a build's groups. Before the groups existed it said unknown group "Team" (). Distribute reads each target group's builds (GET /v1/betaGroups/{id}/relationships/builds), reports internal all-builds groups as "already available" and never sends them the build; unknownGroupMessage points at groups sync for configured groups. APPLE_LIVE_REJECTED (cli/api/client.ts, also refused by storebound api); tests/asc-query-guard.test.ts checks every App Store Connect GET with a query in cli/ against the spec's declared parameters and that list; the fake App Store Connect answers 400 to them (tests/idempotency.test.ts).
41 2026-10 Every Play App content card ended in an owner-only Save, so the owner came back once per card (privacy policy, ads, app access, advertising ID, government, financial, health, target audience, content rating, data safety). Saving an App content declaration is not public: it goes to Google only with the next "Send changes for review" in Publishing overview (verified 2026-10-13). Card Saves are the agent's (declaration-save): declarationSaveAllowed (exactly Save / Kaydet, a button on an /app-content/ page) and flowAllowed for content rating and target audience, while publishingControl refuses "Send changes for review", "Submit N changes for review", Submit, Publish, "Restart review" everywhere; the plans' buttons.final is "Send changes for review"; console fill presses the declaration's Save; console plan play-app-content --all walks every card and ends with one owner list (cli/lib/console-forms.ts, cli/lib/console-plan.ts, cli/lib/console-play-plans.ts, cli/console/fill.ts; tests/console-play-plans.test.ts, tests/browser/console-flow.browser.ts).
40 2026-10 Finding a new app's Play Console app id (play.appId) took several steps: the app list shows 10 apps per page and the new app was not on page 1. The id was in the URL Create app redirects to (/developers//app//app-dashboard) all along. console verify play-app-record --url <url> reads the id from the redirect (or the capture JSON's url), shows the play.appId change as a dry diff and --write upserts it (upsertNestedString); storebound play app-id finds it by package name across captured app list pages (search box or pages) as an agent step (cli/lib/play-app-id.ts, cli/play/app-id.ts; tests/play-app-id.test.ts, a modeled 3-page list).
39 2026-10 Play IARC questionnaire: page 1's Next did nothing until the IARC Terms of Use box was ticked, an owner-only agreement, so the agent stopped mid-run and waited for the owner. The box is in the plan's ONE owner batch before the run with its exact place (ownerBatch, printed first; play-app-content and play-content-rating); the step says not to press Next again and again; console fill waits for the tick ("agreement needed on …", blocksNext) instead of pressing a Next that does nothing (tests/console-play-plans.test.ts, tests/browser/console-flow.browser.ts).
38 2026-10 App Store Connect's New App dialog: a programmatic value set (a form_input-style set) on the Name and SKU textboxes left the dialog's React state empty (""), while clicking the field and typing worked. The apple-app-record plan says "type, don't set value" (uiFacts and the fill step); console fill types every text field key by key and reads it back (typeText in cli/console/fill.ts); tests/browser/console-flow.browser.ts (a New App fixture whose state follows key presses only).
37 2026-10 A scan.ignore entry whose match text was in the evidence (a code line) instead of the finding title accepted nothing and said nothing. init --profile game wrote example-looking values into .storebound/credentials.json (YOUR_KEY_ID, /absolute/path/to/upload.p12, sk_your_revenuecat_v2_secret_key), which commands read as real credentials. scan.ignore[].evidence matches an evidence line ("file:line text"), match stays the title; scan (report notes) and check (warning) name every entry that accepts no finding (ignoreMatches / applyIgnore / unusedIgnores in cli/lib/scan/rules.ts). templates/credentials.example.json holds empty strings only; isPlaceholder (cli/lib/credentials.ts) makes "your-…", <…>, "xxx", "/absolute/path/to/…", "example" and the zero Apple Ads client id missing in every credential read; check warns, doctor shows set / missing / placeholder per key. tests/game-report.test.ts.
36 2026-10 Privacy answers derived from the wrong source: privacy-manifest --propose wrote an EMPTY NSPrivacyCollectedDataTypes while appPrivacy.types listed 5 collected types; console plan apple-app-privacy added "Other Usage Data" from dataSafety (PSL_OTHER_APP_ACTIVITY) although appPrivacy.types was given; console plan play-advertising-id answered "uses advertising ID" Yes because dataSafety declared PSL_DEVICE_ID (a random install id, not the advertising ID). --propose builds NSPrivacyCollectedDataTypes from appPrivacy.types (types, linked, tracking, purposes as Apple's manifest keys; NSPrivacyTracking still only with app-level domains, lesson 29) and check warns on an empty list against collected types. appPrivacy.types is authoritative in the App Privacy sheet (unlisted = not collected, certain); without it, tracking false and no ad / attribution SDK make Advertising Data, Customer Support, Environment Scanning, Hands and Head "not collected" unless a project signal suggests them (cli/lib/project-signals.ts). Advertising ID comes from ad / attribution SDKs, the AD_ID permission (source or merged manifest, or its tools:node="remove"), ads.appAdsTxt or console.appContent.advertisingId; none: No (certain), a conflict: ask (advertisingIdUse). tests/game-report.test.ts.
35 2026-10 scan on a Capacitor game: rule review-prompt (3.2.2(x), reward for rating) fired on const rate = F.genRate(lv, bonus.gen(r)); (a bare "rate" next to "bonus"); every account finding cited import './buildings/register'; as sign-up evidence; and 5.1.1(v) cleared as soon as accounts.inApp named a screen the code did not have. The incentive signal needs a rate-us / leave-a-review phrase next to a reward, a reward "for rating / reviewing", or a reward granted within a few lines of a system review prompt call (rewardNearReviewApi). Sign-in / sign-up / deletion signals skip import, require and export-from lines (codeLine), and auth endpoints must be an absolute path, URL or ${base}/..., never a module path; config-only accounts say "from config (accounts.enabled)". A config claim of in-app deletion without code evidence stays a finding (medium, "claimed by config, not found in code"). tests/game-report.test.ts with tests/fixtures/scan/capacitor-game.
34 2026-10 The macOS desktop release ran the universal app's x86_64 slice on the Apple silicon build Mac (arch -x86_64 storebound-desktop --self-test, and the x64 sidecar's version) to cover both CPU types. That starts Rosetta, and macOS showed the owner "App Update Required: this version of storebound includes a component that will not open in macOS 28". tools/desktop-release.ts never runs an x86_64 slice: sliceProblems checks every Mach-O in the bundle with lipo -archs (each must carry arm64 and x86_64), the self-test and the sidecar version run natively (arm64) only; tests/desktop-release.test.ts (slice check, and no arch -x86_64 / Rosetta launch in the release tools); docs/desktop-release.md.
33 2026-10 Play Console: pressing "Submit N changes for review" in Publishing overview while a review was running opened a dialog "Do you want to restart your review?" with "Restart review". Pressing it restarts the running review with the new changes; the plan did not know the dialog. play-production-release plan: the dialog is a known step and UI fact, "Restart review" is in neverPress (the owner's button), Cancel is allowed (closes it, nothing changes); play-first-release and release-management skills explain it. tests/console-play-plans.test.ts.
32 2026-10 play build passed -PversionCode / -PversionName and android.injected.version.*, but a Capacitor app/build.gradle read versionCode from an env var, so the AAB stayed at versionCode 1 (the post-build check caught it). Checked with AGP 8.13: -Pandroid.injected.version.code=42 on versionCode 1 gives an AAB with versionCode 1, so the injected property does not override defaultConfig for bundleRelease. The Groovy line versionCode (project.findProperty('versionCode') ?: 1) as Integer fails ("Value is null": it parses as versionCode(...) as Integer); the cast belongs inside the parentheses. versionSourceIssues (cli/lib/gradle.ts) reads defaultConfig of the app module's build.gradle(.kts): a literal, an env var or anything but the project property is a warning in play build (before Gradle, also in the dry run and --json as versionWarnings) and in check, with the exact line (Groovy versionCode((project.findProperty('versionCode') ?: <current>) as Integer), versionName project.findProperty('versionName') ?: '<current>'; Kotlin versionCode = (project.findProperty("versionCode") as String?)?.toInt() ?: <current>, versionName = project.findProperty("versionName") as String? ?: "<current>"), both lines built with AGP 8.13; the post-build versionCode check is now a hard stop (the AAB is not copied). tests/build-sign.test.ts, tests/web-assets.test.ts.
31 2026-10 After the owner canceled a WAITING_FOR_REVIEW submission, the app version went DEVELOPER_REJECTED, then PREPARE_FOR_SUBMISSION, and every IAP version stayed DEVELOPER_REJECTED. apple iap-submit --with-version --dry skipped all of them ("latest version is DEVELOPER_REJECTED: change it in App Store Connect (or --new-version)") and said "nothing to submit". Apple takes the same version again: POST /v1/reviewSubmissionItems with that inAppPurchaseVersion → 201 READY_FOR_REVIEW (all 7), then PATCH submitted: true → 200 WAITING_FOR_REVIEW; an empty READY_FOR_REVIEW draft was reused for it. DEVELOPER_REJECTED IAP, subscription and subscription group versions are submittable as they are (planIapSubmission; the app version already was): the plan says "add (developer rejected, re-submitted as is)" and apple resubmit's table the same; --new-version only for APPROVED / REJECTED; an empty draft is reused (chooseSubmission). The fake ASC leaves the versions of a canceled submission DEVELOPER_REJECTED, accepts them as items and refuses APPROVED ones. tests/resubmit.test.ts.
30 2026-10 Resubmitting after an Invalid Binary rejection: apple iap-submit --with-version --yes created a second, empty draft next to the rejected submission (UNRESOLVED_ISSUES, holding the REJECTED version and 7 IAP versions) and stopped with "the store says it already exists but does not list it yet (lists lag)". The answer was really 409 ITEM_PART_OF_ANOTHER_SUBMISSION naming the rejected submission; and right after a new build is attached Apple answers the submit with 409 "Version is not ready to be submitted yet". That 409 lasted 25 minutes with a VALID, APP_STORE_ELIGIBLE build attached and the console's Resubmit / Add for Review disabled: the appStoreVersion item was still REJECTED. PATCH /v1/reviewSubmissionItems/{id} {"data":{"type":"reviewSubmissionItems","id":...,"attributes":{"resolved":true}}} → 200, item READY_FOR_REVIEW, then the submit passed. An empty READY_FOR_REVIEW draft cannot be canceled (409 "Resource is not in cancellable state"). chooseSubmission lists the open submissions (READY_FOR_REVIEW, UNRESOLVED_ISSUES, WAITING_FOR_REVIEW, IN_REVIEW) and reuses one, never creating a second; isConflict excludes the code and addItem names the other submission, a draft the run created is left as is (not cancellable, harmless) and the named one used; REJECTED items are marked resolved before the submit (resolveRejected: the version item in apple resubmit, IAP / subscription items with --resolve-iaps); submitWithRetry re-reads the items on "not ready", stops at once naming a REJECTED item out of scope and how to resolve it, and backs off (15 s, 30 s, 60 s, 2 min, heartbeat, 10 min limit) only when none is left; apple status labels an empty draft harmless and treats its cancel 409 as such; new apple resubmit --build <n> (build by number, plan table, --yes); apple status shows the UNRESOLVED_ISSUES items and the command. tests/resubmit.test.ts.
29 2026-10 Apple refused build 1 as Invalid Binary one minute after submission: "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." The app's own manifest had NSPrivacyTracking true with an EMPTY NSPrivacyTrackingDomains, as storebound recommended from appPrivacy.tracking: true although the tracking came from an ad SDK (LevelPlay) whose own manifest declared it. Apple's wording names the other direction (domains listed, tracking false); both are invalid. After the fix (app manifest false, App Privacy still "used for tracking") check wrongly demanded the app manifest say true. Rule (trackingProblem / trackingIssue / itms91064 in cli/lib/privacy-manifest.ts): a manifest's NSPrivacyTracking and NSPrivacyTrackingDomains agree, in the app's and every SDK's; check, scan (blocker) and privacy-manifest (blocker) fail on either mismatch with the exact ITMS text and never suggest true without domains. App Privacy tracking no longer needs the app manifest's true: check looks for SDK manifests (Pods, frameworks, SwiftPM checkouts, Carthage, Capacitor plugins, the last built IPA; cli/lib/privacy-files.ts) that declare tracking (info), known tracking SDKs in the dependencies (info), else warns. privacy-manifest --propose and the scan proposal write true only with appPrivacy.trackingDomains (the app's own domains), else false. apple upload reads every PrivacyInfo.xcprivacy in the IPA (app root, Frameworks, bundles; XML or binary) and stops before any network call; apple build checks the exported IPA. tests/privacy-tracking.test.ts.
28 2026-10 First App Store release with IAPs: the console route failed an agent (the IAP page's "Add for Review" menu ignored scripted clicks; the version page no longer has an "In-App Purchases and Subscriptions" section), while apple iap-submit --with-version --yes added the IAPs to the draft submission that held 1.0.0 and submitted them first try. App Privacy's Publish opens a "Publish Your App Privacy Responses?" dialog and leaves a "Published … ago by …" line. appstore-first-release and store-iap-setup point to apple iap-submit --with-version (the owner's yes: it submits), never the console. console verify apple-app-privacy reads the "Published …" line as the published signal; the plan names the dialog. tests/console-play-plans.test.ts.
27 2026-10 A real Play first release (an agent with a browser tool) found the steps no API does: the App category stayed "Not selected" (play listing cannot set it: edits.details has no category field); production had 0 countries (edits.countryAvailability is read-only); App content had ten cards with their own pages; the IARC questionnaire is one long page whose Yes answers reveal follow-ups, with an IARC Terms of Use agreement on page 1 and a final "Save" (not "Submit") on the Summary; Play's material checkboxes, like its radios, ignore native input; long evaluate calls timed out at 45 s; "Send changes for review" ignored a JS click; managed publishing is off by default, so approved means live. Plans play-store-settings, play-app-content (+ one per card), play-production-countries, play-production-release; config play.category (Play's lists), play.countries, play.managedPublishing, console.contentRating.details, console.appContent, social, products[].randomRewards; every ask-owner answer in one batch; check warns without a category and counts the open App content answers; validate reports the category reminder and "production has 0 countries"; play status and play release say what is console-only; the IARC Terms box is never-press; material checkboxes get the inner [role=checkbox] click (plan and console fill); every plan ships inPageLoop. tests/console-play-plans.test.ts.
26 2026-10 An agent using storebound spent hours handing the owner one click, command or answer at a time ("press Next", "download the CSV template", "now run the upload", a compliance question in the middle of a run): skills said "let the owner do it" / "by hand" for navigation, exports and uploads. The first rule "Do it, don't ask" heads docs/agents-core.md (every generated adapter), README and the store-pitfalls skill: the agent does API calls, uploads, builds, exports, console navigation and forms itself; the owner presses only the final public or irreversible buttons and answers the facts no file proves in one batch at the start. tests/do-it-dont-ask.test.ts checks the rule is there and fails on skill text that hands navigation, draft saves, exports, uploads or form filling to the owner.
25 2026-10 In a non-interactive shell play build stopped in Gradle with "SDK location not found" (no ANDROID_HOME there, local.properties gitignored) although the SDK was in ~/Library/Android/sdk; apple upload --no-wait --yes sent the IPA for over 10 minutes without a line (progress every 10th part only, no per-part or per-call timeout). resolveAndroidSdk (ANDROID_HOME, ANDROID_SDK_ROOT, sdk.dir in local.properties, the Android Studio default folder; only folders with platforms/ or platform-tools/) hands the SDK to Gradle in the environment, never writes local.properties, stops before Gradle with the places checked; doctor shows it (cli/lib/tools.ts, cli/play/build.ts, cli/doctor.ts). apple upload prints the size, a line per part and a 20 s heartbeat (api, altool, processing wait), per-part and per-call timeouts, --timeout exits 7 with the resume hint (sendParts in cli/apple/upload.ts, cli/lib/progress.ts; tests/tools-path.test.ts, tests/build-sign.test.ts, tests/upload-progress.test.ts).
25 2026-10 apple build --out <shared dir> still used ~/Library/Developer/Xcode/DerivedData, and the app binary plus the archive's dSYMs / Symbols/*.symbols held the home folder, user name and checkout path (543 privacy-gate hits). --derived-data / default <out>/DerivedData, --path-free (Swift and clang prefix maps) with an archive scan that stops before the export: cli/lib/path-free.ts, tests/path-free.test.ts; store-pitfalls 124.
24 2026-10 flow run release --apply --yes ran apple upload --no-wait, which printed "not a terminal: re-run with --yes to confirm" and declined, yet the flow logged "4/13 upload: ok" and went on to apple testflight wait for a build that was never uploaded: a declined confirmation exited 0, and the flow's --yes never reached the steps. confirm() (cli/lib/ctx.ts): every decline (answered no, not a terminal without --yes, under serve) prints declined: <reason> and exits 3 (DECLINED_EXIT). flow run reports "step N declined: " (status declined) and stops; --apply --yes adds --yes to write steps only (planStep, declinedReason; tests/flow.test.ts).
23 2026-10 Capacitor: native build ships old web assets. flow run release --apply went straight into apple build (xcodebuild archive) with ios/App/App/public and android/app/src/main/assets/public 3.5 hours older than the latest source commit: the native projects hold the copy npx cap sync made last, and nothing ran the web build + cap sync first, so the old game would have shipped. New build.prebuild (per store apple.prebuild / play.prebuild): apple build / play build run it first (printed, streamed, nonzero exit stops; --dry only prints; --no-prebuild; once per flow run through STOREBOUND_PREBUILD_DONE). After it, a Capacitor project's copied public/index.html must be newer than the last commit outside ios/ android/ node_modules/ store/ .storebound/ and <webDir>/index.html, else the build stops ("web assets are older than the source ..."; --allow-stale-web overrides). check warns when stale without a prebuild; build output and dry plan show the bundle's sha256 + mtime. Monorepo (apps/native with its own capacitor.config.ts, android/, ios/; play build failed with "no Gradle project ... in android or ."): the one Capacitor project one or two levels down is found and named, several ask for play.build.dir / apple.build.* (cli/lib/web-assets.ts, tests/web-assets.test.ts).
22 2026-10 play build built a good AAB (Capacitor 8 / AGP), then failed its own check with "AAB manifest without package / versionCode". The aapt2 proto manifest was read with element field numbers one too high (name 4, attribute 5, child 6 instead of Resources.proto's 3, 4, 5). The test fixture came from the same wrong encoder, so the tests passed. aabInfo / encodeProtoManifest in cli/lib/gradle.ts follow Resources.proto. tests/build-sign.test.ts builds the manifest bytes by tag, without the encoder, and the parser was checked against real AGP bundles. Lesson: a parser's fixture must not come from the same code's encoder.
21 2026-10 Play Data safety: conditional required rows. play data-safety for a game with username accounts (accountCreation: ['PSL_ACM_USER_ID_OTHER_AUTH']) got 400 "Invalid safety labels declaration: Response missing for PSL_ACCOUNT_DELETION_URL": once account creation is not PSL_ACM_NONE, Play requires the delete-account URL and the data-only deletion question (PSL_SUPPORT_DATA_DELETION_BY_USER, and PSL_DATA_DELETION_URL when it is yes), and refuses PSL_HAS_OUTSIDE_APP_ACCOUNTS; storebound wrote none of them. Later (2026-10-12) a game got 400 "Line 7: Response ID missing" for PSL_SUPPORT_DATA_DELETION_BY_USER,,FALSE: the current form asks it as a single choice (DATA_DELETION_YES / DATA_DELETION_NO / DATA_DELETION_NO_AUTO_DELETED) and no longer has PSL_DATA_COLLECTION_USER_REQUEST_DELETE; the CSV must use the current form's Response IDs. dataSafetyRows writes them from accounts.deletionUrl and dataSafety.partialDeletion / partialDeletionUrl; DATA_SAFETY_RULES / typeRules / dataSafetyIssues in cli/lib/data-safety.ts check every conditional row in check, project.declarations and play data-safety (dry and real, before the POST); tests/pure.test.ts (conditional required rows), tests/idempotency.test.ts, tests/app-privacy.test.ts. The single choice is written one row per option (`partialDeletion: true
20 2026-10 Hours of selector-fix cycles of the scripted console engine (console fill): the real console kept differing from the model (App Privacy "Set Up" as plain text, purpose checkboxes without an accessible name, two information steps in a type dialog, Play's material radios that ignore the native input). Agent-first console mode: console plan <form> gives an AI agent the start URL, steps, answer sheet with the console's labels, never-press list and verified UI facts; the agent drives the page with its own browser tool and stops before the final button; console verify <form> checks the result (App Privacy summaries from the page text, Play Create app from input states, API for the App Store record and age rating). The scripted fill stays a fast path (and now handles the verified facts). MCP: console_plan, console_verify (read-only). tests/console-plan.test.ts, tests/browser/console-flow.browser.ts.
19 2026-10 play images --dry aborted with "en-US: no sevenInchScreenshots ... (render first)" on a phone-only project: Play's 7" and 10" tablet screenshots are optional (required: 512 icon, 1024x500 feature graphic, at least 2 screenshots; Play Console Help answer 9866151), but every image type was treated as required. play images uploads what exists (icon, feature graphic, phone screenshots, tablet / TV / Wear only when rendered), prints an info line for missing optional sets, never deletes what Play has for them without --prune; play.tablets (default: tablet renders exist) switches tablets for shots render / shots check / shots gallery / validate / play images, and true with missing renders is an error; then a 404 "Listing for language 'de-DE' not found." for languages still status=draft: play images takes the languages play listing pushes and skips one with no Play listing as info (noListing) (cli/play/images.ts langImages, resolvedTablets in cli/shots/report.ts, tests/play-post-reads.test.ts).
18 2026-10 console fill play-app-record with a copy of the owner's Chrome cookies reached the Play account chooser once, then Google answered /v3/signin/rejected and the chooser: Google session cookies are device-bound (Chrome's Device Bound Session Credentials) and Google signs out sessions whose cookies look stolen, which can also sign the owner out of their real Chrome. Never copy Google cookies by default: storebound's own profile is the default (the owner signs in once by hand), started by storebound as a normal Chrome (no --enable-automation, navigator.webdriver false) and attached over CDP; --use-chrome-session google|apple is an explicit opt-in that warns; /signin/rejected is said plainly (cli/console/session.ts, tests/browser/console-session.browser.ts). Confirmed 2026-10-12: the earlier default copy DID sign the owner out of Play Console in their real Chrome.
17 2026-10 After the data types were saved, the real App Privacy page shows each type's "Set Up " as a plain <p> in a div with a click handler (no button role, no link) and puts the types in a box after the Data Types heading's box: role-based locators and the "nearest heading" check found nothing, so no type was set up. A "?" (More information) button sits next to each type. setUpAllowed / setUpTexts (cli/lib/console-forms.ts: exact text "Set Up " of a type storebound answers, in the Data Types section, outside dialogs) and setUpClick / setUpType / flowWhere (last heading before the control) in cli/console/fill.ts; the per-type dialog steps are page sections, so an unknown step dumps and pauses instead of ending the run; tests/fixtures/console/apple-app-privacy-setup.html (structure of the real page) and tests/browser/console-flow.browser.ts.
16 2026-10 console fill play-app-record with the copied Chrome cookies printed only "still waiting for the sign-in" for 5+ minutes on a signed-in Play Console: Google pages embed accounts.google.com frames (the account widget, cookie rotation), and the detector counted any frame on a sign-in host. signInReason: a sign-in page only by its top-level URL, a visible password / email-entry form in the main frame, or a visible Apple sign-in frame (idmsa / appleid); other frames never count. Each detection logs "sign-in detected because: at "; heartbeats name the page; a copied session that lands on a sign-in URL is said plainly ("did not carry over"); the watchdog dumps the sign-in wait once after --step-timeout, --debug-dump up to 3 times (tests/browser/console-fill.browser.ts, chooser / home with the widget frames, a top-level accounts.google.com page).
15 2026-10 The real App Privacy "Data Collection" dialog names each data type checkbox by its label run together with its help text ("NameIncluding first or last name"), and labels some types differently from Apple's help page ("Purchases", "Other Data"): exact-name matching found none. labelNames (exact name, then the label as a prefix that ends where the help text starts) and TYPE_ALIASES in cli/lib/console-forms.ts; the flow fixture mirrors the real dialog's structure (tests/browser/console-flow.browser.ts).
14 2026-10 console fill apple-app-privacy clicked Next onto a real console page it did not recognise and then waited 8 hours without a single log line. Every wait of the console engine is bounded: a step watchdog (--step-timeout, 30 s) writes a sanitized debug dump to .storebound/console-debug/ and prints the page's buttons and the manual steps; a line a minute while waiting; no progress for 20 min stops the run with exit 1; --debug-dump dumps every step (cli/console/debug.ts, tests/browser/console-flow.browser.ts, which also scans the engine for waits without a deadline).
13 2026-10 App Privacy purposes were over-mapped: Play "advertising" became both Third-Party Advertising and Developer's Advertising or Marketing (Device ID, Coarse Location, Product Interaction, Performance Data). applePurpose maps one Play purpose to one Apple purpose (advertising: third-party only); Developer's Advertising only from appPrivacy.types; check compares appPrivacy.types with Info.plist / PrivacyInfo.xcprivacy (tests/app-privacy.test.ts).
12 2026-10 A new Play app has no API: Play Console's "Create app" is the only way, its page-level button carries the same words as the form's final submit, and Free cannot become Paid later. console fill play-app-record: createAppAllowed lets the opener through only on the app list, never in a form or dialog (EN + TR); declarations never ticked; Free / Paid outlined as an owner check (tests/browser/console-fill.browser.ts). Verified 2026-10-12: the form is at /developers/<id>/create-new-app and has an App package name field with a read-only "Check availability" (filled from play.packageName; the package name no longer comes from the first AAB upload); App or game / Free or paid are <material-radio>s whose model only updates on a click of the inner [role=radio] (the engine clicks it; console verify play-app-record checks aria-checked and the "Select …" hints). After Create app the owner adds the new app to the service account (app-level permissions do not cover later apps).
11 2026-10 Google Play has no public search API, so there is no Play keyword rank or Play competitor list to read without scraping. aso reads App Store search only (iTunes Search API); aso audit covers Play through the listing text (short description, density); the aso-basics skill says so.
10 2026-10 The iTunes Search API order is close to, not equal to, App Store search (no personalisation, no search ads, no in-app event rows) and it has no subtitle field. aso rank / aso competitors / aso audit label ranks as a trend and show no competitor subtitles; popularity only from Apple Ads, never estimated (cli/lib/aso-intel.ts).
9 2026-10 A retried or repeated write can duplicate on a live store: a POST create retried after a timeout or 5xx, a re-run during the list lag right after a create, two runs at once, a Play edit redone after an unclear commit, a crash in the middle of a screenshot set. Every write is an upsert (cli/lib/upsert.ts: findOrCreate with a fresh re-check and a journal of recent creates, createOnce treating 409 as found, no blind retry of a create); API clients retry only requests that are safe twice; Play edits are all or nothing; asset sets keep the matching prefix; one lock per app and store (cli/lib/lock.ts, --force-unlock). Table: docs/idempotency.md; tests/idempotency.test.ts runs every write twice and asserts zero writes on the second run.
8 2026-10 Capacitor plugins in a monorepo live in apps/native/node_modules, so scan missed them. scan / privacy-manifest resolve workspace packages (cli/lib/scan/project.ts, cli/lib/node-packages.ts).
7 2026-10 Console UIs: the App Store Connect sign-in runs in an idmsa.apple.com iframe; "User Access" is a plain heading, not a labeled group; Chrome drops session cookies without --restore-last-session. console session and fill (cli/console/), tests/console-*.test.ts.
6 2026-10 Screenshot fonts: only part of the glyphs a language needs were available, so Turkish / Polish / Vietnamese characters fell back to another font silently. shots fonts / shots check check every language's characters against the fonts (tests/browser/glyphs.browser.ts).
5 2026-10 Account features do not imply email collection; Data safety said "email collected" without evidence. scan marks email only with code evidence, else "ask owner" (cli/lib/scan/declare.ts).
4 2026-10 (Superseded by 43: the limit counts characters.) The App Store keyword limit (100) was taken to count UTF-8 bytes. See 43.
3 2026-10 App Store app names are unique per storefront: "already being used" although the developer owns no such app. apple.name (App Store name with a suffix the owner picks); app.name stays the brand.
2 2026-10 Dry runs that print no plan ("listing: 20 locales") break "show the dry run to the owner". cli/lib/dry.ts dryPlan(title, rows); tests/pitfalls.test.ts scans cli/** for --dry branches that print only a count (allowlist with TODOs for commands being fixed).
1 2026-10 App Store Connect's /inAppPurchasesV2 list lags right after apple iap creates a product; apple iap-review then skipped it silently. apple iap-review names config products missing from the store's list and exits 1; apple iap and play products re-read the list after creating and warn (cli/lib/store-lag.ts, tests/pitfalls.test.ts).

Adding an entry

  1. A row above (next number, month, one-sentence lesson, the guard with file or test names).
  2. A section in skills/store-pitfalls/SKILL.md: symptom → cause → what to do → which command guards it. Then bun tools/gen-adapters.ts.
  3. Where the code can know it: a check (check, validate, scan, a warning in the command) and a test with recorded store answers that fails without the fix.