- `storebound console plan <form> [--json] [--lookup]`: read-only plan for an AI agent that drives the browser itself: start URL (deep links), ordered steps, answer sheet with the console's exact labels (certain / ask owner, curly quotes noted), never-press list, verified UI facts, driving guidance (in-page async loop + window.__log), verify instructions. All six forms. - `storebound console verify <form> [--from-text <file|->]`: App Privacy per-type summaries from page text, Play Create app from input states, content rating / target audience summaries, App Store record and age rating from the API; exit 1 on a mismatch. - MCP: console_plan, console_verify (read-only); console fill stays off MCP. - App Privacy (verified 2026-10-12): two information steps, unlabeled purpose checkboxes (nearest parent text), "Definitions and Examples" never pressed; fixture and engine follow. - Play Create app (verified 2026-10-12): /developers/<id>/create-new-app, App package name + read-only Check availability, material-radio inner [role=radio] click, app dashboard URL and play.appId, service account "Add app" as an owner step. - console-autofill skill rewritten agent-first; agents-core rule 10; README; pitfalls 12, 18 (confirmed sign-out), 19; CHANGELOG; play-first-release.
75 KiB
Changelog
Unreleased
-
Agent-first console mode.
storebound console plan <form> [--json](read-only, offline;--lookupasks the App Store Connect API for the app id) gives an AI agent everything to fill a console form with its own browser tool: the start URL (deep links from apple.appId, play.developerId, the new play.appId and the remembered ids), ordered steps (what to click by visible text), the answer sheet (each answer certain or ask-owner, with the console's exact labels, curly quotes noted), the never-press list (Publish, Submit, Create, agreements, help panels), the known UI facts per form and how to verify.storebound console verify <form> [--from-text <file|->](read-only, exit 1 on a mismatch) checks the result: App Privacy from the page text (per-type "Used for …", linked, tracking), Play Create app from the input states, content rating and target audience from their summaries, the App Store record and age rating from the API. MCP:console_plan,console_verify(read-only;console fillstays off MCP). The console-autofill skill now puts the agent-driven mode first;console fillis the optional scripted fast path. Pitfall 20. -
App Privacy, verified on a real console (2026-10-12): two information steps in each type dialog ("Let’s define two important terms…", "To help put tracking into context…"), purpose checkboxes without an accessible name (matched by the nearest parent text), "Definitions and Examples" never pressed. The scripted engine handles all of it.
-
Play Create app, verified through Claude in Chrome (2026-10-12): the form's own URL
/console/u/0/developers/<id>/create-new-app(the engine opens it directly with a developer id); the App package name field (fromplay.packageName, max 150) and its read-only "Check availability" ("Package name available"), which storebound presses; the<material-radio>gotcha (the native input does not update the form's model: the engine clicks the inner[role=radio]); the app dashboard/developers/<id>/app/<appId>/app-dashboard(the app id is remembered and deep-links the App content forms); after Create app the owner adds the new app to the service account (Users and permissions > the service account > Add app), never an agent. The old note "the first AAB upload sets the package name" is gone. -
Confirmed (2026-10-12): the earlier default copy of the owner's Google cookies DID sign the owner out of Play Console in their real Chrome (pitfall 18). The copy has been opt-in since the change below.
-
Fix (
play images): a phone-only project no longer aborts with "no sevenInchScreenshots ... (render first)". Play requires the 512 icon, the 1024 x 500 feature graphic and at least 2 screenshots (Play Console Help answer 9866151); 7" / 10" tablet screenshots are optional.play images(dry and real) uploads what exists (icon, feature graphic, phone screenshots; tablet, TV and Wear sets only when rendered) and printstablet screenshots: none (optional; the app isn't eligible for tablet featuring without them). Missing required sets are still errors, with the fix.- New
play.tablets?: boolean(default: true when tablet renders exist, else false).false:shots render,shots check,shots gallery,validateandplay imagesnever ask for tablet sets.true: rendered, and missing ones are errors (local renders and the listing on Play). - A missing optional set never deletes the screenshots Play has for it: the dry run shows
keeps <type> on Play; new--prunedeletes them (shown aswould deletein the dry run; not exposed over MCP). Pitfall 19.
- New
-
Safety (
console fill): storebound no longer copies the owner's Chrome cookies by default. A copied Google session reached Play Console once, then Google answered/v3/signin/rejected: Google binds its session cookies to the device (Chrome's Device Bound Session Credentials) and signs out sessions whose cookies look stolen, which can sign the owner out of their real Chrome. Sources: blog.google/security/protecting-cookies-with-device-bound-session-credentials , knowledge.workspace.google.com/admin/security/investigate-and-take-action-on-suspicious-session-cookies .- Default: storebound's own profile per store (
~/.storebound/console-profile/<store>); the owner signs in once by hand (the first run prints what to do) and it is kept. - storebound starts that Chrome itself (
--user-data-dir,--remote-debugging-port=0) and attaches over CDP: no--enable-automation,--disable-blink-features=AutomationControlled(navigator.webdriverfalse), none of Playwright's launch switches, Chrome's own user agent. Graceful close by CDPBrowser.close. --use-chrome-session google|apple: explicit opt-in to the old copy, now of only that store's cookies; Google's prints a warning about device-bound sessions. Apple's is opt-in too (Apple publishes no binding, but no guarantee).- Google's
/signin/rejectedpage is said plainly: "Google refused sign-in in this window". - The desktop dialog no longer says it uses the Chrome sign-in. Pitfall 18.
- Default: storebound's own profile per store (
-
App Privacy purposes (wrong answers in
console fill apple-app-privacy,project.declarationsandscan): Play "advertising" became both Third-Party Advertising and Developer's Advertising or Marketing (Device ID, Coarse Location, Product Interaction, Performance Data). Now one Apple purpose per Play purpose, following Apple's definitions (developer.apple.com/app-store/app-privacy-details/): advertising -> Third-Party Advertising, analytics -> Analytics, personalization -> Product Personalization, functionality / fraud / account / communications -> App Functionality. Developer's Advertising or Marketing is No unless the owner says so; with a Play "communications" purpose console fill asks. -
New
appPrivacy.types: the owner's App Privacy answers per Apple data type id (all 35: USER_ID, DEVICE_ID, GAMEPLAY_CONTENT, ADVERTISING_DATA, CUSTOMER_SUPPORT, ENVIRONMENT_SCANNING, HANDS, HEAD ...), each{ collected?, linked?, tracking?, purposes? }(THIRD_PARTY_ADVERTISING, DEVELOPERS_ADVERTISING, ANALYTICS, PRODUCT_PERSONALIZATION, APP_FUNCTIONALITY, OTHER_PURPOSES), validated against Apple's lists.console fill apple-app-privacyfills them as certain answers (they win over what dataSafety suggests); types with no Play equivalent work from the block alone; anything unset stays "(you decide)".scan --writeproposescollected+purposesfor detected types the block does not answer; neverlinked/tracking(the SDK disclosures do not prove them).checkcompares the block with the iOS project, both ways: a type used for tracking needsNSUserTrackingUsageDescriptionandNSPrivacyTrackingtrue (and the reverse is flagged), and the app'sNSPrivacyCollectedDataTypes(manifest keys mapped to App Privacy ids) must match the types marked collected.- templates/app-privacy.md, the store-declarations, console-autofill and store-pitfalls skills document it.
-
console fill play-app-record: Play Console's Create app form, the Play counterpart ofapple-app-record(the Play Developer API cannot create apps).- Account:
play.developerId, the workspace account's id or the remembered one; else the only account the login sees; else the chooser entry namedplay.developerAccount(new optional field: the account's display name, e.g. an organisation account). Several accounts with none set, or a name that matches none or two: it stops and lists them. Remembered in~/.storebound/console-accounts.json. - Navigation: All apps > the page-level Create app button, through a dedicated allowlist (
createAppAllowed: exact name "Create app" / "Uygulama oluştur", button or link, on the app list, no Create app form open, not inside a form or dialog). The form's own Create app stays refused. - Fills App name (
play.name, new, elseapp.name; 30 characters checked), Default language (Play Console label, e.g. "English (United States) – en-US"; matched by code in a Turkish console), App or game (profile) and Free or paid (play.free, new, else Free for IAP / ads only) as an owner check: outlined, with the note that a free app can never become paid. The Developer Program Policies and US export laws declarations are never ticked (anagreementfield is outlined whatever its answer). The log and guidance say the package name is set by the first AAB upload (play upload --track internal/flow run release). - In
--dry, theconsole filllist, the MCP / desktop form list and the desktop App info "Play Console forms" card.
- Account:
-
shots checkno longer reportsi drawn with a system fontfor captions with ligatures ("Outfits" in en / de / nl with Montserrat). The font usage probes (one span per character, read with CDPCSS.getPlatformFontsForNode) sat side by side in one line, so Chrome shaped "f" + "i" into the fi ligature across them and the lone "i" probe reported no glyph of ours. Each probe is now its own block. The Google Fontstext=subsets were fine (they keep the GSUB ligatures); regression test with a real Montserrattext=subset (tests/glyph-ligatures.test.ts). -
--help/-hon every command, subcommand and group (storebound shots render --help,storebound apple --help) prints the usage (the module's usage lines, the description and the options it accepts) and exits 0 before anything loads; it used to start the command (a full render). An unknown flag (--dyr) exits 2 with the usage before anything runs (cli/data/flags.json,bun tools/gen-flags.ts).storebound shotsalone lists the shots commands.storebound ads apple ...is reachable again (the plainadscommand used to swallow it). -
shots render --scenestakes scene ids (the usage said numbers); an unknown id fails at once with the list of ids instead of a 30 s page timeout. -
Faster CI:
bun run ciruns as a task graph (independent steps at once,[step]-prefixed output, fail fast, time per step) with the tests split into parallel shards (CPU cores - 1, max 8) balanced by recorded durations, browser suites in shards of their own. Newbun run ci:quickfor the inner loop: typecheck plus the tests related to the change against origin/main (through imports) plus scrub. The desktop Rust host runs only whensrc-taurior the sidecar build changed (CI=fullalways; Forgejo CI sets it). Fixed sleeps in tests became event waits. -
ASO from official and public data only (no scraping, no third-party ASO services):
aso competitors [--term <kw>] [--storefront us,tr] [--top 10]: per keyword of the locale's keyword field (and--term) the top apps in the iTunes Search API (name, developer, rating count, first release, last update, genre localized to the storefront), the words in their names that the name / subtitle / keywords do not cover (only words of two or more developers: a one-developer word is likely a brand) and the apps new in a term's top N since the last snapshot (.storebound/aso/competitors/<date>.json). Same pace asaso rank(one call every 3.5 s, stop on 403 / 429). Competitor subtitles are not in any public API and are not shown.aso suggest [--locale] [--fresh] [--offline] [--write]: keyword candidates from the own name / subtitle / description (words and two-word phrases), competitor names, the localized category names (from the Search API'sgenres) and, with Apple Ads credentials, the Apple Ads Platform API keyword suggestions (POST /v1/suggestions/keywords/query, relative popularity 0-100). Each candidate shows relevance (own text, competitor names), competition (top apps with the word in their name) and popularity (Apple Ads only, else "unknown"). Proposes a keyword field per locale that fits 100 bytes with the existing rules (no spaces, no duplicates, plural-insensitive, no name / subtitle words, no filler or category words; current terms kept first unless--fresh) as old -> new;--writeputs it intostore/appstore.mdwith a draft line thatcheck --asoandaso auditreport until the owner deletes it.aso audit: one offline report:check --asofindings, the cross-locale free keyword room, rank trend arrows from theaso ranksnapshots, competitor coverage gaps, Play short description use and keyword density, App Store conversion from the analytics snapshots, Product Page Optimization / Play listing experiment ideas (suggestions only: PPO cannot test the subtitle; Play experiments stay in Play Console) and the next 3 actions.- MCP: read-only
aso_competitors(saves no snapshot),aso_suggest(never writes),aso_audit(offline). - Desktop: an ASO card on the Growth tab (
aso.audit): keyword field state, keywords ranked, coverage gaps, the rank trend of the best three keywords (visx, rank 1 at the top), the rank table and the next 3 actions. examples/demo-apphas listing files (store/appstore.md,store/listing.md) to try the ASO commands on.
-
Local font files in
shots.fonts:title,bodyandfallback[]take, besides a Google Fonts family, a path (relative to the config) or{ family, file, weight?, style?, license? }(TTF / OTF / WOFF / WOFF2, validated by extension and signature; the family comes from the name table when not given).shots fontschecks the file's cmap per language, tries the listed fallbacks in order before the automatic Noto ones (pl title: ę ł ą Ł not in ER Display -> Noto Sans Display), errors when nothing covers a character, and records the file in fonts.json with its path, sha256 and license. Render andshots videoserve it in place at/fonts/local/...(never copied); the hash in fonts.css invalidates the capture cache, and render refuses a font file changed sinceshots fonts.shots checktreats the local font and listed fallbacks as configured and warns when a local font has no license note. -
Idempotency: every store write is an upsert; running a command twice, or again after a crash or a lost answer, leaves exactly one of each thing (table of every write path:
docs/idempotency.md).- New
cli/lib/upsert.ts:findOrCreatelooks again with a fresh GET right before a create and asks a journal of recent creates (.storebound/created.json, a direct GET by id) so a re-run during the store's list lag reuses what the last run made;createOncetreats 409 / ALREADY_EXISTS as found and never sends a create blindly again after a timeout or 5xx (it looks first; only keys the store keeps unique get one more try). - The App Store Connect, Google Play, RevenueCat and
storebound apiclients retry 5xx / network errors only for requests that are safe twice (GET, PUT, PATCH, DELETE, relationship adds, state actions, price schedules); a POST create without a clear answer throwsAmbiguousWriteError. Plan runners treat a 409 duplicate as done and stop on an unclear create instead of retrying. - Uses it in
apple iap,apple subscriptions(groups, subscriptions, localizations, promotional offers, offer code campaigns, custom codes, review screenshot by MD5),apple offer-codes,apple cpp create,apple events create,apple ppo create,apple testflight groups,apple bundle,apple review-details(only changed fields),apple release --phased on,apple adhoc register/profile,apple signing ensure(a lost certificate answer keeps the key and finds the certificate by its public key),apple upload(a COMPLETE build upload is not sent again, an unfinished one is resumed),analytics setup,reviews send,revenuecat sync,play subscriptions(subscriptions and offers). - Review submissions (
apple iap-submit,cpp submit,events submit): the draft is reused or created once, an item already in it is not added again. - Screenshot and preview sets keep the leading files that already match (MD5, position, delivered), replace the rest in order and fix the order if Apple shows another; a crash midway converges on the next run without duplicates.
- Play edits: one open at a time per run, deleted on every error path, committed only on success, deleted instead of
committed when nothing changed (
ctl.unchanged()); redone only when nothing went live, never after a commit without a clear answer.play imagescompares sha256 first and replaces only changed image types;play releasereuses a versionCode Play already has and skips identical track releases;play rolloutcommits nothing when the track is already like that;play uploadis done when the versionCode is on the track already. - One writer per app and store: write runs take
.storebound/locks/<store>-<app>.lock(dry runs and reads do not); a second run stops with who holds it; a lock of a process that is gone is taken over;--force-unlock. tests/idempotency.test.tswith stateful store doubles (tests/helpers/fake-stores.ts): write commands run twice with zero writes on the second run, a crash in the middle of a screenshot set, IAP and subscription creates whose answer is lost, 409 duplicates, the list lag after a crash, Play edit error paths and an unclear commit, the lock. Pitfall 9 indocs/pitfalls.mdand the store-pitfalls skill.- The last open rows are closed (
docs/idempotency.mdhas none left):apple listing: the App Store version is created throughfindOrCreate(a fresh look right before the create, the journal for the list lag, a lost answer looks first); localizations throughcreateOnce(409 = found, then only the differing fields are PATCHed); unchanged localizations and an attached build are left alone.play listing: compares again inside the write edit and PUTs only differing details and languages; nothing left to change = the edit is deleted, not committed.apple appinfo: categories, age rating, content rights compared with the live values, only differences PATCHed.play data-safety: Play cannot read the form back, so the sha256 of the last uploaded CSV is kept in.storebound/data-safety.sha256; the same CSV is not sent again unless--force(MCPforce).- Twice-run tests for
apple listing(plus a lost version create and a 409 localization),play listing(plus a store that became current before the edit),apple appinfo,play data-safety,apple promoted,apple win-back,apple preorder,apple testflight testers,play uploadandprices import(Play).
- New
-
Faster
shots capture(web) andshots render(a 22-language game took about 50 min to capture, serially, with a new context and a full game boot per frame):--concurrency N(andshots.capture.concurrency): parallel browser contexts in one Chromium; defaults from the CPU count (capture min(3, cores / 4), render min(4, cores / 2)). Output files do not depend on the order; progress lines carry language / device / frame, n/total and an ETA. Render runs ffmpeg asynchronously and makes the contact sheets after the images.- Per-frame timing (goto, waitFor, eval, delay, screenshot, total) in each progress line and in
.storebound/capture-report.json, with time per step and the slowest frames at the end. shots.capture.session: 'per-language': one page per language and device, booted once; shots switch withnavigate: 'spa'(pushState + popstate, then waitFor / eval) or'reload'; a failed precondition falls back to a fresh page (reuseTimeoutMs).- Input-hash cache (
.storebound/capture-cache.json: shot entry, URL, frame size, session, copy text, app build fromshots.capture.buildor the served HTML + scripts, fonts): unchanged frames are skipped;--only-missing,--force. MCPshots_capture(concurrency,mode) andshots_render(concurrency).
-
apple listing --dry,play listing --dryandapple appinfo --dryprint a real plan (also--json), read-only: the version decision ofapple listing(keep, update the editable version's string while PREPARE_FOR_SUBMISSION, create, or refuse: number taken, not higher than a reviewed version, another version in review / pending release), the build to attach, and per locale / language create or update with old -> new per field (long text cut with its length, unchanged fields summarized); app info name / subtitle / privacy URL, categories, age rating and content rights the same way. Locales whose section isstatus=draftorstatus=translate(listing translate) are skipped with the reason;--include-draftssends them. The real run prints the same plan, asks in a terminal and applies exactly it (unchanged localizations are no longer PATCHed, Play PUTs only changed languages); what's new is left out on an app's first version. -
apple.name: the App Store name in the primary locale (at most 30 characters, validated) for when the plain name is taken on the App Store ("already being used").app.namestays the Play and on-device name.console fill apple-app-recordfills Name from it (elseapp.name), andapple listing,apple listing pull(a pulled name equal to it is not written into the file), the listing completeness view,check --aso, the console app picker andscan's beta-wording rule use it for every locale without its own### nameblock.checkreports an App Store name over 30 characters in any locale and hints when every locale uses the plainapp.name(hintsin--json). -
Pitfalls from real use become guardrails: new skill
store-pitfalls(read before any store task, linked from the agent rules) anddocs/pitfalls.md(changelog of lessons; CONTRIBUTING: every real-use bug adds an entry and, where possible, a check or test).apple iap-reviewnames config products missing from App Store Connect's list (it lags a minute after a create) and exits 1 instead of skipping them silently;apple iapandplay productsre-read the list after creating and warn when a new product is not in it yet.cli/lib/dry.tsdryPlan(title, rows): the standard dry-run plan table.apple iap --drynow reads localizations, price schedule and availability and prints what a real run would add;apple iap-reviewandplay productsend their dry runs with the table. A test scanscli/**for--drybranches that print only a count (allowlist with TODOs for commands being fixed elsewhere).
-
Screenshot glyphs (a Turkish render showed fallback glyphs for "ş" / "ğ" while
shots checkpassed): the cause was not subsetting (css2text=already takes glyphs from every subset) but the title font: Lilita One has no ş ğ İ and most of Latin Extended-A, so Chrome silently drew them with another font.shots fontschecks every character of every language (copy.json + config languages) against the cmap of the faces that draw it (new pure-TS cmap reader for TTF / OTF / TTC / WOFF / WOFF2, no dependency), adds an automatic Noto fallback per missing character (Noto Sans Display for titles at a heavy weight, Noto Sans, Noto Sans