iOS ASO Copilot
Keep the app's marketing work continuous: inspect the existing evidence, reconcile what is actually
live, improve the next decision, and record changes. A report is not evidence that marketing worked.
Read references/provenance.md before factual claims. Use references/respectaso.md when RespectASO
is requested or available; discover its actual MCP tools before choosing a fallback.
Resolve the app and the single state store
SKILL_DIR is the real directory containing this skill (resolve symlinks).
- From the app repo run
git worktree list --porcelain. The first worktree entry is the main
checkout; verify it belongs to the current repository. Do not infer this from missing branch brackets.
STORE is that checkout's marketing/, shared by all its worktrees. Never infer it from the skill's
repository or create a second store in a worktree.
- Read
config.md for the exact App ID and working version. Read STATE.md, queue.md, hypotheses,
recent decisions, metrics and the relevant handbook sections. State is a cache, not a data source.
- Prices, bundle IDs, locales, credentials, seasonal assumptions and tool access are per app. Never
carry them from a previous project. Missing identity means report that gap before app-specific API calls.
- Keep one global skill installation. Project links may point to it; do not copy skill directories.
Preserve unique local additions before replacing a copy. The installer and agents must resolve links.
Open every run with the ledger, before proposing anything
The first command of any invocation — status, manual or auto — is:
python3 "$SKILL_DIR/scripts/ledger.py" --store "$STORE" report
Three sections in a fixed order: what is already live and what happened to it, what moved, what to
do next. Do not draft a new hypothesis before section A is on screen. Proposing while live
experiments sit unjudged is exactly how a store accumulates 21 open hypotheses and zero verdicts;
the ledger exists to make that state visible instead of comfortable.
- A · Hypothesis ledger. Every hypothesis carries a state:
judged / due / open /
not shipped. due means its declared window has closed — judge it this run, or record the
specific missing evidence. A queued status on a change that is already public is a discrepancy
to fix, not a row to skip: check the live listing and the shipped metadata package, not STATE.md.
no markets: or no queries: on a rank hypothesis is an incomplete legacy record, not evidence
that the test has no scope. Add the verified storefronts and exact prospective query basket before
interpreting it; never reconstruct either field from marketing prose automatically.
The script never writes a verdict; you write it, against the evidence table in
references/aso-loop.md. An adverse verdict names the exact revert diff and the version that
would carry it — a rollback nobody can execute is not a decision.
- B · Progress and regress per key. Every tracked key, paired first-to-latest. A censored
observation (absent within the queried depth) never becomes a numeric delta; entering or leaving
the queried depth is reported as its own event, because those are the two most informative
outcomes and averaging them into zero hides both.
- C · What to do next. Ordered by net proceeds per paying subscriber in that storefront × the
headroom left in the current position band, every input printed on its own row. This is a
priority order, not a revenue forecast, and it is deliberately not sorted by impressions:
a first place in a storefront that nets $6.37 a subscriber is worth less than a fifth place in
one that nets $29.88. Storefronts with no price record rank nowhere rather than ranking at zero.
Drafting the next one is the same command with draft --market X --queries "a; b". It scaffolds
only the deterministic half — id, storefront, basket, each query's real baseline, the window dates,
a kill criterion carrying those numbers, the storefront's net proceeds, and every collision with
work already in flight or already judged there. It refuses outright when the store has no
observation for a query in that storefront, because a hypothesis whose basket is never queried
cannot be judged, and that refusal is the whole point: the window would otherwise close on nothing.
change and mechanism stay blank — a generated mechanism is a guess wearing a hypothesis's
clothes, and it is the one part that must be argued.
The ledger's inputs are $STORE/metrics/ranks.csv (append-only observations, one row per
date × market × keyword, carrying depth and request status) and $STORE/metrics/markets.csv
(per-territory customer price and Apple proceeds, pulled from ASC — never estimated). Refresh the
first with one command — ledger.py refresh --bundle <id>, which re-queries the basket ranks.csv
already holds and ingests the result; add --failed-only to pick up what a rate limit ate. Apple
documents the Search API at roughly 20 calls a minute and 429s above it, so the default 3s between
queries is not a knob to turn down: at 1s more than half the queries came back as failures. Refresh
markets.csv when prices change.
Respect the requested scope
| Request |
Behavior |
| Status or review only |
Read-only; do not initialize or mutate the store |
| Review and fix docs or skill |
Complete the authorized local changes and validate them |
| Manual or guided iteration |
Explain choices at the decision points the user wants to control |
| Auto iteration |
Complete one cycle below; record unavailable inputs and continue independent work |
The user's authorization persists across turns. Do not require magic words such as auto for an
explicit editing request. Public messages, publishing, account changes and spending follow the
actual authorized scope and references/commands.md; prepare a concrete diff first. Do not ask
again for permission already given. If an action cannot proceed, name the specific missing condition.
Reconcile release status separately from measurement readiness
Read asc versions list --app "$APP_ID" --platform IOS before interpreting asc validate.
Track the live version and working/review version separately. An update in review does not
stop observation of the already published app. READY_FOR_DISTRIBUTION may fail an editability check
because it is already live; that does not turn a live app into a prelaunch blocker.
Use P0-prelaunch when no version is live, P1-review for the candidate's review status, and P2-cold or
P3-measure only with available measurement evidence. If metrics are absent, say live; volume unknown.
A 100-download weekly planning threshold is not a statistical test and cannot prove a sample is
sufficient. Judge precision for the selected market, metric, baseline and expected effect. Record
launch volatility, release changes, price changes and paid traffic as possible confounds; a fixed
three-week wait does not automatically create an uncontaminated baseline.
If a refresh fails, state which observation is cached, its age, and the error. Failure is not zero,
not "unranked", not a healthy funnel, and not permission to populate examples as actual data.
Status output
Give a compact account of live/candidate versions, open ASO and channel hypotheses, observed metrics
with units and dates, gaps in the ledger, pending actions, and one useful next step. Use
scripts/funnel_visualizer.py --store "$STORE" for an evidence table; select a market and segment
when required. Do not infer page conversion, paid subscribers, source shares or bank payouts from
unjoined totals. Do not diagnose a bottleneck from a generic benchmark.
One execution cycle
- Verify identity and live/candidate versions. Reconcile stale state within the authorized edit scope.
- Fetch the weekly data that is accessible. Save source exports, complete periods, units, filters,
timezone and recording time. Missing fields stay blank with a reason. Never invent a baseline.
- Refresh a fixed query basket. Record exact query, storefront, timestamp, source, method, depth,
request status and app identity. Keep new discovery queries separate from paired comparisons.
- Review hypotheses whose declared windows have closed. Use
references/aso-loop.md. Early status
is allowed; an early win or a causal verdict without adequate evidence is not.
- Inspect hypothesis files, staged metadata and the live listing for collisions. Include queued
changes that may already have shipped. Map locales to affected storefronts: they are not isolated
simply because their country codes differ. Preserve the original prediction and add dated amendments.
- Prepare the next justified change. Read the exact live fields, specify which fields change, check
relevance and native-language quality, validate limits, and produce a reviewable diff. Separate a
multi-field localization release from a one-variable experiment. Draft a prospective criterion when
requested; never backdate it or pretend a retrospective criterion was preregistered.
- Complete already-authorized actions. Queue only actions requiring new authorization or unavailable
prerequisites, with a concrete artifact and explanation. Do not let one blocker halt independent work.
- Update state, decisions and references consistently; report done, missing evidence and next action.
- Schedule verification in TickTick. When hypotheses are sent to release (or when an App Store version with staged hypotheses is submitted for review), automatically create a scheduled verification task in TickTick (if available) for the end of the measurement window (e.g. went_live + window_days). Multiple hypotheses from the same release or window are batched into a single task in the app's TickTick project (e.g.
🎛<App Name> or configured project ID) with target positions, baselines, and CLI command to run.
Evidence rules that affect decisions
- RespectASO
popularity can mix Apple data and an internal fallback. Preserve popularity_detail,
selected source, fallback flag, exact language/term and date. Scores do not transfer across translations.
- iTunes API order is discovery evidence. It is not verified organic device rank, install volume, or
a basis for an "easy Top 3" or "high ROI" claim. The bundled rank script no longer fabricates these scores.
- Sparse written reviews, RSS recency, app age and lifetime ratings cannot establish query demand,
download velocity, ad spend or an incumbent's inactivity. No formula promises a rank from N reviews.
- Brand terms, competitor brands and relevant generic terms belong to different research groups.
Rank for our own name does not prove generic demand or brand awareness.
- Apple Ads query reports describe paid traffic; ASC Search can include ads. Organic installs per
exact keyword are not directly observed in ASC. Browse is context, not a randomized control.
- Product claims must match shipped behavior. Reject keywords implying unsupported features. Use
honest native review requests; never buy, incentivize, gate or selectively solicit positive reviews.
- A 21-day window is a scheduling default. An inconclusive result can remain inconclusive after it.
Prefer a small number of useful tests over many low-volume markets with unmeasurable predictions.
Reference map
| Need |
Read |
| Claim sources and uncertainty |
references/provenance.md |
| RespectASO tools and data boundaries |
references/respectaso.md |
| Formats and append rules |
references/state-store.md |
| Closing a window, rollback, priority order |
references/aso-loop.md |
| ASC commands and external actions |
references/commands.md |
| Keyword selection and experiment verdicts |
references/aso-loop.md |
| Metrics and denominators |
references/funnel-analytics.md |
| Paid acquisition scenarios |
references/apple-ads.md |
| PPO, CPP, reviews and content |
references/playbooks.md |
| Off-store channels |
references/channel-playbooks.md |
| Short video scripts and briefs |
references/ugc-playbook.md |
| Per-app teaching and reasoning |
$STORE/HANDBOOK.md |
| Missing capabilities and acceptance criteria |
ROADMAP.md |
Bundled scripts
All use the Python standard library and provide --self-check. Run only what the task needs.
ledger.py: the run-opening report above; ingest appends a snapshot to metrics/ranks.csv and
refuses any position deeper than the result list that query returned — a value carried over from an
older snapshot is not an observation.
ledger.py draft: the scaffolding above; refuses an unmeasurable basket, warns on collisions.
ledger.py refresh: re-queries the tracked basket per storefront and ingests it; --failed-only
retries what a rate limit ate. Every command reports the store rows it could not use, not just
report.
funnel_visualizer.py: observed counts with explicit absence; no automatic health or cash estimate.
rank_audit.py: iTunes discovery snapshots, exact bundle identity and per-query error status.
Supply --bundle, --niche and target --markets; existing seed profiles are examples, not app identity.
diff_snapshots.py: comparable JSON query pairs only; legacy missing provenance blocks numeric claims.
harvest_keywords.py: autocomplete candidates and visible competitor metadata; no measured volume.
economics.py: explicit sensitivity scenario, not a forecast or an instruction to increase spending.
campaign_link.py: local campaign links and channel ledger; a generated URL is not a published campaign.
Initialize a missing store only when authorized: copy missing files from assets/store-template/
without overwriting existing content. Fill in the app's identity before running app-specific commands.
1---2name: ios-aso-copilot3description: AI Copilot and free alternative to Astro MCP for this repo's App Store app: keyword rank tracking, live search hints, competitor audit, ASO / keyword iteration, App Store release readiness, Apple Ads setup and economics, product-page experiments, reviews, and content backlog. Reads a persistent state store from the current repo's own marketing/ directory and answers 'where am I, what's next' — never starts from zero.4---56# iOS ASO Copilot78Keep the app's marketing work continuous: inspect the existing evidence, reconcile what is actually9live, improve the next decision, and record changes. A report is not evidence that marketing worked.10Read `references/provenance.md` before factual claims. Use `references/respectaso.md` when RespectASO11is requested or available; discover its actual MCP tools before choosing a fallback.1213## Resolve the app and the single state store1415- `SKILL_DIR` is the real directory containing this skill (resolve symlinks).16- From the app repo run `git worktree list --porcelain`. The first `worktree` entry is the main17 checkout; verify it belongs to the current repository. Do not infer this from missing branch brackets.18- `STORE` is that checkout's `marketing/`, shared by all its worktrees. Never infer it from the skill's19 repository or create a second store in a worktree.20- Read `config.md` for the exact App ID and working version. Read `STATE.md`, `queue.md`, hypotheses,21 recent decisions, metrics and the relevant handbook sections. State is a cache, not a data source.22- Prices, bundle IDs, locales, credentials, seasonal assumptions and tool access are per app. Never23 carry them from a previous project. Missing identity means report that gap before app-specific API calls.24- Keep one global skill installation. Project links may point to it; do not copy skill directories.25 Preserve unique local additions before replacing a copy. The installer and agents must resolve links.2627## Open every run with the ledger, before proposing anything2829The first command of any invocation — `status`, `manual` or `auto` — is:3031```bash32python3 "$SKILL_DIR/scripts/ledger.py" --store "$STORE" report33```3435Three sections in a fixed order: what is already live and what happened to it, what moved, what to36do next. **Do not draft a new hypothesis before section A is on screen.** Proposing while live37experiments sit unjudged is exactly how a store accumulates 21 open hypotheses and zero verdicts;38the ledger exists to make that state visible instead of comfortable.3940- **A · Hypothesis ledger.** Every hypothesis carries a state: `judged` / `due` / `open` /41 `not shipped`. `due` means its declared window has closed — judge it this run, or record the42 specific missing evidence. A `queued` status on a change that is already public is a discrepancy43 to fix, not a row to skip: check the live listing and the shipped metadata package, not `STATE.md`.44 `no markets:` or `no queries:` on a rank hypothesis is an incomplete legacy record, not evidence45 that the test has no scope. Add the verified storefronts and exact prospective query basket before46 interpreting it; never reconstruct either field from marketing prose automatically.47 The script never writes a verdict; you write it, against the evidence table in48 `references/aso-loop.md`. An `adverse` verdict names the exact revert diff and the version that49 would carry it — a rollback nobody can execute is not a decision.50- **B · Progress and regress per key.** Every tracked key, paired first-to-latest. A censored51 observation (absent within the queried depth) never becomes a numeric delta; entering or leaving52 the queried depth is reported as its own event, because those are the two most informative53 outcomes and averaging them into zero hides both.54- **C · What to do next.** Ordered by net proceeds per paying subscriber in that storefront × the55 headroom left in the current position band, every input printed on its own row. This is a56 priority order, not a revenue forecast, and it is deliberately not sorted by impressions:57 a first place in a storefront that nets $6.37 a subscriber is worth less than a fifth place in58 one that nets $29.88. Storefronts with no price record rank nowhere rather than ranking at zero.5960Drafting the next one is the same command with `draft --market X --queries "a; b"`. It scaffolds61only the deterministic half — id, storefront, basket, each query's real baseline, the window dates,62a kill criterion carrying those numbers, the storefront's net proceeds, and every collision with63work already in flight or already judged there. **It refuses outright when the store has no64observation for a query in that storefront**, because a hypothesis whose basket is never queried65cannot be judged, and that refusal is the whole point: the window would otherwise close on nothing.66`change` and `mechanism` stay blank — a generated mechanism is a guess wearing a hypothesis's67clothes, and it is the one part that must be argued.6869The ledger's inputs are `$STORE/metrics/ranks.csv` (append-only observations, one row per70`date × market × keyword`, carrying depth and request status) and `$STORE/metrics/markets.csv`71(per-territory customer price and Apple proceeds, pulled from ASC — never estimated). Refresh the72first with one command — `ledger.py refresh --bundle <id>`, which re-queries the basket `ranks.csv`73already holds and ingests the result; add `--failed-only` to pick up what a rate limit ate. Apple74documents the Search API at roughly 20 calls a minute and 429s above it, so the default 3s between75queries is not a knob to turn down: at 1s more than half the queries came back as failures. Refresh76`markets.csv` when prices change.7778## Respect the requested scope7980| Request | Behavior |81|---|---|82| Status or review only | Read-only; do not initialize or mutate the store |83| Review and fix docs or skill | Complete the authorized local changes and validate them |84| Manual or guided iteration | Explain choices at the decision points the user wants to control |85| Auto iteration | Complete one cycle below; record unavailable inputs and continue independent work |8687The user's authorization persists across turns. Do not require magic words such as `auto` for an88explicit editing request. Public messages, publishing, account changes and spending follow the89actual authorized scope and `references/commands.md`; prepare a concrete diff first. Do not ask90again for permission already given. If an action cannot proceed, name the specific missing condition.9192## Reconcile release status separately from measurement readiness9394Read `asc versions list --app "$APP_ID" --platform IOS` before interpreting `asc validate`.95Track the **live version** and **working/review version** separately. An update in review does not96stop observation of the already published app. `READY_FOR_DISTRIBUTION` may fail an editability check97because it is already live; that does not turn a live app into a prelaunch blocker.9899Use P0-prelaunch when no version is live, P1-review for the candidate's review status, and P2-cold or100P3-measure only with available measurement evidence. If metrics are absent, say `live; volume unknown`.101A 100-download weekly planning threshold is not a statistical test and cannot prove a sample is102sufficient. Judge precision for the selected market, metric, baseline and expected effect. Record103launch volatility, release changes, price changes and paid traffic as possible confounds; a fixed104three-week wait does not automatically create an uncontaminated baseline.105106If a refresh fails, state which observation is cached, its age, and the error. Failure is not zero,107not "unranked", not a healthy funnel, and not permission to populate examples as actual data.108109## Status output110111Give a compact account of live/candidate versions, open ASO and channel hypotheses, observed metrics112with units and dates, gaps in the ledger, pending actions, and one useful next step. Use113`scripts/funnel_visualizer.py --store "$STORE"` for an evidence table; select a market and segment114when required. Do not infer page conversion, paid subscribers, source shares or bank payouts from115unjoined totals. Do not diagnose a bottleneck from a generic benchmark.116117## One execution cycle1181191. Verify identity and live/candidate versions. Reconcile stale state within the authorized edit scope.1202. Fetch the weekly data that is accessible. Save source exports, complete periods, units, filters,121 timezone and recording time. Missing fields stay blank with a reason. Never invent a baseline.1223. Refresh a fixed query basket. Record exact query, storefront, timestamp, source, method, depth,123 request status and app identity. Keep new discovery queries separate from paired comparisons.1244. Review hypotheses whose declared windows have closed. Use `references/aso-loop.md`. Early status125 is allowed; an early win or a causal verdict without adequate evidence is not.1265. Inspect hypothesis files, staged metadata and the live listing for collisions. Include queued127 changes that may already have shipped. Map locales to affected storefronts: they are not isolated128 simply because their country codes differ. Preserve the original prediction and add dated amendments.1296. Prepare the next justified change. Read the exact live fields, specify which fields change, check130 relevance and native-language quality, validate limits, and produce a reviewable diff. Separate a131 multi-field localization release from a one-variable experiment. Draft a prospective criterion when132 requested; never backdate it or pretend a retrospective criterion was preregistered.1337. Complete already-authorized actions. Queue only actions requiring new authorization or unavailable134 prerequisites, with a concrete artifact and explanation. Do not let one blocker halt independent work.1358. Update state, decisions and references consistently; report done, missing evidence and next action.1369. **Schedule verification in TickTick.** When hypotheses are sent to release (or when an App Store version with staged hypotheses is submitted for review), automatically create a scheduled verification task in TickTick (if available) for the end of the measurement window (e.g. went_live + window_days). Multiple hypotheses from the same release or window are batched into a single task in the app's TickTick project (e.g. `🎛<App Name>` or configured project ID) with target positions, baselines, and CLI command to run.137138## Evidence rules that affect decisions139140- RespectASO `popularity` can mix Apple data and an internal fallback. Preserve `popularity_detail`,141 selected source, fallback flag, exact language/term and date. Scores do not transfer across translations.142- iTunes API order is discovery evidence. It is not verified organic device rank, install volume, or143 a basis for an "easy Top 3" or "high ROI" claim. The bundled rank script no longer fabricates these scores.144- Sparse written reviews, RSS recency, app age and lifetime ratings cannot establish query demand,145 download velocity, ad spend or an incumbent's inactivity. No formula promises a rank from N reviews.146- Brand terms, competitor brands and relevant generic terms belong to different research groups.147 Rank for our own name does not prove generic demand or brand awareness.148- Apple Ads query reports describe paid traffic; ASC Search can include ads. Organic installs per149 exact keyword are not directly observed in ASC. Browse is context, not a randomized control.150- Product claims must match shipped behavior. Reject keywords implying unsupported features. Use151 honest native review requests; never buy, incentivize, gate or selectively solicit positive reviews.152- A 21-day window is a scheduling default. An inconclusive result can remain inconclusive after it.153 Prefer a small number of useful tests over many low-volume markets with unmeasurable predictions.154155## Reference map156157| Need | Read |158|---|---|159| Claim sources and uncertainty | `references/provenance.md` |160| RespectASO tools and data boundaries | `references/respectaso.md` |161| Formats and append rules | `references/state-store.md` |162| Closing a window, rollback, priority order | `references/aso-loop.md` |163| ASC commands and external actions | `references/commands.md` |164| Keyword selection and experiment verdicts | `references/aso-loop.md` |165| Metrics and denominators | `references/funnel-analytics.md` |166| Paid acquisition scenarios | `references/apple-ads.md` |167| PPO, CPP, reviews and content | `references/playbooks.md` |168| Off-store channels | `references/channel-playbooks.md` |169| Short video scripts and briefs | `references/ugc-playbook.md` |170| Per-app teaching and reasoning | `$STORE/HANDBOOK.md` |171| Missing capabilities and acceptance criteria | `ROADMAP.md` |172173## Bundled scripts174175All use the Python standard library and provide `--self-check`. Run only what the task needs.176177- `ledger.py`: the run-opening report above; `ingest` appends a snapshot to `metrics/ranks.csv` and178 refuses any position deeper than the result list that query returned — a value carried over from an179 older snapshot is not an observation.180- `ledger.py draft`: the scaffolding above; refuses an unmeasurable basket, warns on collisions.181- `ledger.py refresh`: re-queries the tracked basket per storefront and ingests it; `--failed-only`182 retries what a rate limit ate. Every command reports the store rows it could not use, not just183 `report`.184- `funnel_visualizer.py`: observed counts with explicit absence; no automatic health or cash estimate.185- `rank_audit.py`: iTunes discovery snapshots, exact bundle identity and per-query error status.186 Supply `--bundle`, `--niche` and target `--markets`; existing seed profiles are examples, not app identity.187- `diff_snapshots.py`: comparable JSON query pairs only; legacy missing provenance blocks numeric claims.188- `harvest_keywords.py`: autocomplete candidates and visible competitor metadata; no measured volume.189- `economics.py`: explicit sensitivity scenario, not a forecast or an instruction to increase spending.190- `campaign_link.py`: local campaign links and channel ledger; a generated URL is not a published campaign.191192Initialize a missing store only when authorized: copy missing files from `assets/store-template/`193without overwriting existing content. Fill in the app's identity before running app-specific commands.