Merge pull request #3 from masa2146/feat/market_research_skill

Feat/market research skill
This commit is contained in:
masa2146 2026-06-22 01:15:43 +03:00 committed by GitHub
commit 0c0224d493
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 2068 additions and 0 deletions

View File

@ -35,6 +35,19 @@
"license": "Apache-2.0",
"keywords": ["android", "clone", "feasibility", "effort-estimation", "market-analysis"],
"category": "security"
},
{
"name": "market-research",
"source": "./plugins/market-research",
"description": "Research the app/game market and surface scored, non-repeating clone candidates that feed into clone-app.",
"version": "0.1.0",
"author": {
"name": "masa2146"
},
"repository": "https://github.com/masa2146/clone-app-skill",
"license": "Apache-2.0",
"keywords": ["market-research", "app-discovery", "trends", "clone"],
"category": "security"
}
]
}

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
__pycache__/
*.pyc

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,228 @@
# Market-to-Build Pipeline — Design Spec
**Date:** 2026-06-22
**Status:** Approved (brainstorming complete)
**Scope:** Two new plugins (`market-research`, `hermes-build`) plus a one-branch edit to the
existing `clone-app` plugin, chaining into a full *discover → analyze → build* pipeline.
## 1. Purpose
Give the user an end-to-end loop:
1. **Discover** — autonomously research the app/game market and surface ≥10 fresh,
scored clone candidates that do not repeat across runs.
2. **Pick** — the user selects one or more candidates.
3. **Analyze** — each pick flows into the existing `clone-app` feasibility skill.
4. **Build** — on approval, hand off to a new `hermes-build` skill that generates the
capability bundle [hermes-agent](https://github.com/nousresearch/hermes-agent) lacks
(Firebase, Play publish, ads) and best-effort drives hermes to build → test →
Firebase → ads → upload to the Play **internal testing** track.
Effort throughout stays measured in **AI Sprints** (one focused session), never calendar time —
consistent with the existing clone-app convention.
## 2. The hard constraint (unchanged)
`plugins/android-reverse-engineering/` stays byte-identical to upstream. Before any commit:
```bash
git status --porcelain plugins/android-reverse-engineering/ # must print nothing
```
All new work lives under `plugins/market-research/` and `plugins/hermes-build/`. The only
edit to shared/existing files: `clone-app`'s Phase 7 (one new branch) and root
`.claude-plugin/marketplace.json` (two new plugin entries).
## 3. hermes-agent — what it is, what it lacks
Established by research (github.com/nousresearch/hermes-agent, MIT, v0.17.0):
- **Is:** a self-hosted agent framework + runtime. Executes shell/Python/build tools/tests/git
across six backends (local, Docker, SSH, Singularity, Modal, Daytona). Any LLM provider
(no vendor lock-in). Interfaces: CLI (`hermes`), Python SDK (`from run_agent import AIAgent`),
HTTP gateway.
- **Lacks (no built-in tool):** Firebase integration, Google Play publisher automation, AdMob/ads
configuration, Android emulator orchestration. These are added via **custom hermes skills**
(`~/.hermes/skills/`), **MCP servers**, or **shell delegation** (`firebase`, `gcloud`,
`bundletool`, `fastlane`).
**Consequence:** "fully autonomous dev→publish" is not a built-in button. `hermes-build` generates
the missing capabilities as a bundle, then lets hermes execute them. This is the integration model
the user chose (generate skill + MCP config bundle).
## 4. Architecture
```
plugins/
android-reverse-engineering/ [upstream, untouched]
clone-app/ [existing — only Phase 7 gains one branch]
market-research/ [NEW]
hermes-build/ [NEW]
```
**Pipeline (chained handoffs, no central orchestrator):**
```
/market-research
→ free web gather + LLM trend synthesis, history-excluded
→ ≥10 scored ideas → user picks N
→ for each pick: offer "run clone-app feasibility?" → invoke clone-app skill
clone-app (existing 8 phases, unchanged)
→ Phase 7 decision gate, NEW third branch "Build with hermes"
→ invoke hermes-build (passing report + stack + payloads.json)
hermes-build
→ preflight (best-effort) → mission brief → capability bundle → drive hermes
→ signed AAB → Play internal testing track → build-report
```
Handoff style matches the existing `clone-app → writing-plans` chain. Each plugin is independently
installable and testable.
## 5. Decisions locked in brainstorming
| # | Decision | Choice |
|---|----------|--------|
| Scope | How much to build | Full pipeline, end to end |
| Data source | Market research inputs | Free web (WebSearch + Play charts + App Store RSS) **+** LLM trend synthesis. $0, no paid intel API. |
| Freshness | Avoid repeat ideas | History file; each run excludes/deprioritizes prior suggestions |
| Ranking | What makes a good candidate | Composite score: **Cloneability + Market opportunity + Monetization fit** (primary); niche gap (secondary tiebreaker) |
| Hermes integration | How we drive hermes | Generate hermes **skill + MCP config bundle** that fills hermes's gaps |
| Publish boundary | How far autonomy goes | Build signed AAB → **Play internal testing track only**. Stops before production. Reversible. |
| Packaging | Repo layout | **Three plugins** (market-research, clone-app, hermes-build) |
| Orchestration | Who drives the chain | **Chained handoffs** — each skill offers the next |
| Missing prereqs | hermes/creds not set up | **Best-effort + skip** with warnings. Never half-fails. |
## 6. Plugin: `market-research`
**Goal:** autonomous market scan → ≥10 scored, non-repeating clone candidates.
**Skill:** `skills/market-research/SKILL.md` — phased prose orchestrator (clone-app pattern).
**Phases:**
- **P0 — Seed rotation.** Pick varied search angles this run (category × region × niche), read
from `references/research-angles.md` (rotating list, e.g. hyper-casual games, utilities,
fintech LATAM, AI tools, health). Variety comes from rotated seeds + history exclusion.
- **P1 — Gather (free web).** WebSearch + scrape: Play top-charts pages, App Store RSS feeds
(`itunes.apple.com/<region>/rss/topfreeapplications`), ProductHunt/Reddit/trend articles.
Helper `fetch-charts.py` (stdlib, offline-fixture-tested) returns structured chart data; the
model synthesizes emerging trends from the search results.
- **P2 — Synthesize candidates.** LLM clusters findings into ≥10 distinct ideas; each carries:
name, category, what-it-does, why-now (trend signal), incumbent(s), monetization model.
- **P3 — Score.** Composite per `references/scoring-guide.md`: cloneability (tech-stack-simplicity
guess, backend surface, no heavy ML), market opportunity (demand/growth/weak incumbents),
monetization fit (ads/IAP, ARPU category) — all primary; niche gap secondary. Weighted 0100,
ranked.
- **P4 — History dedup.** Exclude/deprioritize anything in
`./work/market-research/history.json`. Append new picks after presentation.
- **P5 — Present + handoff.** Show ranked table (≥10). User picks N. For each pick, resolve to a
real Play package/URL (Play search if the synthesized idea named an app without a package — the
downstream `extract-package.sh` needs a concrete package), then offer "run clone-app feasibility
on this?" → invoke `clone-app`.
**State** (user cwd, never inside the plugin) — `./work/market-research/`:
- `history.json` — every past suggestion (package/name + date + run-id): the non-repeat memory.
- `research-<YYYY-MM-DD>.md` — this run's full report.
**Scripts** (`scripts/`, each testable, stdlib-only):
- `fetch-charts.py` — chart/RSS fetch → JSON; `--html-file` / `--json-file` for offline fixtures.
- `history.py` — read / append / dedup `history.json`.
**References** (`references/`): `research-angles.md`, `scoring-guide.md`, `report-template.md`.
**Tests** (`tests/`): chart/RSS fixtures, history-dedup test, scoring sanity, `smoke-structure.sh`.
Offline-fixture pattern, no network — same discipline as clone-app.
## 7. Plugin: `clone-app` (edit only)
Single change: **Phase 7 decision gate** gains a third branch.
Existing branches: **Yes**`superpowers:writing-plans`; **No** → stop.
New branch: **"Build with hermes"** → invoke `hermes-build`, passing the clone report, the
user-selected stack (Phase 4), and `$WORK/payloads.json` (backend API surface).
No other clone-app file changes. The upstream RE plugin is not touched.
## 8. Plugin: `hermes-build`
**Goal:** take a clone-report + plan, generate the capability bundle hermes lacks, then best-effort
drive hermes to build → test → Firebase → ads → Play internal-testing track.
**Skill:** `skills/hermes-build/SKILL.md` — phased.
**Phases:**
- **P0 — Preflight (best-effort, never blocks).** Probe for: `hermes` binary, `firebase-tools`,
`fastlane`/`bundletool`, Play service-account JSON, signing keystore, AdMob app/unit IDs. Write
`preflight.json` (available/missing). Each missing capability marks its later steps `SKIP` with a
warning; the pipeline continues.
- **P1 — Generate mission brief.** `mission-brief.md` = structured spec hermes consumes: app name,
target stack, feature list, backend API surface (from `payloads.json`), Firebase services needed,
ads placement, build target = **signed AAB → Play internal testing only** (stops before
production).
- **P2 — Generate hermes capability bundle.** Emit into `./work/<pkg>/hermes/`:
- hermes **skills** (hermes skill format): `firebase-setup`, `play-publish`
(fastlane supply → internal track), `admob-wire`, `android-build` (gradle / AAB sign).
- **MCP config** stub (Firebase CLI / Play API where MCP fits).
- `hermes.config` snippet (backend = Modal/Docker, model provider).
- Steps whose prereq was missing in P0 are marked `SKIP` in the brief.
- **P3 — Drive hermes (best-effort).** If `hermes` present: feed the mission brief to hermes
(`hermes` CLI / SDK `AIAgent.run_conversation`), stream progress — hermes autonomously scaffolds →
builds → tests → Firebase init → ads wire → signs AAB → uploads internal track. If `hermes`
absent: skip the drive, hand the user the bundle + brief + run instructions.
- **P4 — Report.** `build-report-<YYYY-MM-DD>.md`: what ran, what skipped (and why — missing creds),
AAB path, internal-track upload status, and explicit next manual steps. Promotion to production is
manual and out of scope.
**State** — `./work/<pkg>/hermes/`: bundle files, `mission-brief.md`, `preflight.json`,
`build-report-<date>.md`.
**Scripts** (`scripts/`, testable): `preflight.sh` (capability probe → JSON), `gen-bundle.py`
(templates → hermes skill files; template-driven, fixture-tested), `drive-hermes.sh` (invoke hermes
if present, else print instructions).
**References:** `bundle-templates/` (firebase / play / admob / android-build skill templates),
`mission-brief-template.md`.
**Tests:** preflight-probe (mock present/missing), gen-bundle output vs fixtures,
`smoke-structure.sh`. No live hermes / Play / Firebase calls in tests.
**Boundaries (explicit):**
- Never auto-promotes past the internal testing track to production.
- Never spends money or publishes without pre-existing user credentials.
- Missing creds → skip + warn, never a hard fail.
- Legal/publishing liability surfaced in the build-report; the existing clone-app legal note still
governs which apps may be analyzed/cloned at all.
## 9. Cross-cutting conventions (inherited from clone-app)
- **Working dir** is `./work/...` relative to the user's cwd — never inside any plugin.
- **Scripts** use `#!/usr/bin/env bash`, run via `bash <path>`; Python is **stdlib-only**
(`urllib`, `json`, `re`); no pip, no virtualenv.
- **Tests** use `set -uo pipefail`, aggregate failures, exit non-zero if any fail. Python scrapers
tested offline against `tests/fixtures/` via `--html-file` / `--json-file`. New scrape logic needs
a fixture, not a live call.
- **Commits** follow Conventional Commits scoped per plugin:
`feat(market-research): …`, `feat(hermes-build): …`.
- **Marketplace:** add both plugins to root `.claude-plugin/marketplace.json`; each plugin carries
its own `.claude-plugin/plugin.json`.
## 10. Build order (each gets its own implementation plan)
This umbrella spec decomposes into three implementation plans, built and tested in order:
1. **`market-research`** — standalone value first; hands to existing clone-app manually until the
chain edit lands.
2. **`clone-app` Phase 7 edit** — one branch; small.
3. **`hermes-build`** — largest; depends on the brief/bundle contract.
Each plan is produced via the `writing-plans` skill and verified by its own test suite before the
next begins.
## 11. Out of scope
- Paid market-intelligence APIs (SensorTower / data.ai / AppMagic).
- Autonomous production publishing, live AdMob spend, or any irreversible Play Console action.
- Installing/bootstrapping hermes or its credentials on the user's machine (best-effort detect only).
- Modifying the upstream `android-reverse-engineering` plugin.
- iOS build/publish (Play/Android only).

View File

@ -0,0 +1,13 @@
{
"name": "market-research",
"version": "0.1.0",
"description": "Autonomously research the app/game market (free web + LLM trend synthesis) and surface scored, non-repeating clone candidates that feed into the clone-app skill.",
"author": {
"name": "masa2146"
},
"repository": "https://github.com/masa2146/clone-app-skill",
"license": "Apache-2.0",
"keywords": ["market-research", "app-discovery", "trends", "clone", "play-store", "app-store"],
"skills": "./skills/",
"commands": "./commands/"
}

View File

@ -0,0 +1,37 @@
# market-research
A Claude Code plugin that autonomously researches the app/game market and surfaces
scored, non-repeating clone candidates, then hands user-chosen candidates to the
`clone-app` plugin for feasibility analysis.
## What it does
`/market-research` runs a 6-phase workflow:
0. **Seed rotation** — pick varied search angles (category × region × niche) so runs differ.
1. **Gather** — App Store RSS chart data (`fetch-charts.py`) + free web search for trends.
2. **Synthesize** — cluster findings into ≥10 distinct app/game ideas.
3. **Score** — composite score: cloneability + market opportunity + monetization fit.
4. **Dedup** — exclude anything already in `./work/market-research/history.json`.
5. **Present + handoff** — show the ranked table; chosen candidates flow into `clone-app`.
## State
Written under `./work/market-research/` in your current directory:
- `history.json` — every past suggestion; the non-repeat memory.
- `research-<date>.md` — the full report for a run.
## Scripts
- `scripts/fetch-charts.py` — fetch App Store RSS chart feeds → normalized JSON.
- `scripts/history.py` — read / append / dedup the suggestion history.
Python is stdlib-only. Tests run offline against `tests/fixtures/`:
```bash
bash plugins/market-research/tests/run-all.sh
```
## Effort convention
Effort is measured in **AI Sprints** (one focused Claude session), never calendar time.

View File

@ -0,0 +1,30 @@
---
allowed-tools: Bash, Read, Glob, Grep, Write, Edit, WebFetch, WebSearch, Skill
description: Research the app/game market and surface scored, non-repeating clone candidates
user-invocable: true
argument-hint: [optional focus, e.g. "casual games" or "fintech LATAM"]
argument: optional market focus or angle (optional)
---
# /market-research
Run the market-research workflow: scan the market, score candidates, hand picks to clone-app.
## Instructions
Follow the market-research skill workflow in
`${CLAUDE_PLUGIN_ROOT}/skills/market-research/SKILL.md` exactly, phases 0 through 5.
### Step 1: Optional focus
If the user passed a focus argument (e.g. "casual games", "fintech LATAM"), bias
the Phase 0 seed selection toward it. Otherwise rotate seeds normally.
### Step 2: Run the skill
Execute Phase 0 → Phase 5 from SKILL.md. Pause for the user at Phase 5 (pick
candidates to hand to clone-app).
### Step 3: Deliver
Ensure the report is written to `./work/market-research/research-<date>.md` and
the new suggestions are appended to `./work/market-research/history.json`. For
each candidate the user picks, resolve it to a Google Play package/URL and invoke
the `clone-app` skill on it.

View File

@ -0,0 +1,107 @@
---
description: Autonomously research the app and game market — rotate search angles, pull App Store chart feeds, synthesize emerging trends, score candidates by cloneability + market opportunity + monetization fit, exclude anything suggested before, and hand chosen candidates to the clone-app skill. Use when the user wants market research, fresh app/game ideas to clone, trending apps, or "what should I build next". 中文触发词市场调研、找应用创意、热门应用、值得克隆的app
trigger: market research|app ideas|what to build|what should i clone|trending apps|find apps to clone|top apps|market scan|市场调研|应用创意|热门应用
---
# Market Research — Discover Clone Candidates
Scan the app/game market with free sources, score candidates, and hand the ones
you pick to the `clone-app` skill. Every run rotates its search angles and
excludes everything suggested before, so results stay fresh.
This skill orchestrates 6 phases (05). Deterministic steps are factored into
helper scripts under `${CLAUDE_PLUGIN_ROOT}/skills/market-research/scripts/`;
AI-judgment steps follow rubrics under `.../references/`.
## Legal note
This produces market research and ideas only. Actual cloning is gated later by
the `clone-app` skill's own legal note (analyze only apps you are authorized to).
## State & working dir
All state lives under `./work/market-research/` in the user's cwd (never inside
the plugin):
- `history.json` — every candidate ever suggested (the non-repeat memory).
- `research-<YYYY-MM-DD>.md` — this run's report.
Create it: `WORK="./work/market-research"` and `mkdir -p "$WORK"`.
Pick a `RUN_ID` for this run that encodes the chosen angles (e.g.
`2026-06-22-games-br`); it is stored with each suggestion so future runs can see
which angles were used recently.
## Phase 0: Seed rotation
Read `${CLAUDE_PLUGIN_ROOT}/skills/market-research/references/research-angles.md`.
If `$WORK/history.json` exists, skim recent `run_id`s to see which angles were
used lately. Choose 23 categories, 12 regions, and 1 niche lens you did NOT use
last run. If the command passed a focus argument, force one category to match it.
State the chosen angles to the user in one line before continuing.
## Phase 1: Gather (free web)
Hard chart data — for each chosen region, pull the relevant feeds:
```bash
python3 ${CLAUDE_PLUGIN_ROOT}/skills/market-research/scripts/fetch-charts.py \
topfreeapplications --region <region> --limit 25 > "$WORK/charts-<region>-free.json"
python3 ${CLAUDE_PLUGIN_ROOT}/skills/market-research/scripts/fetch-charts.py \
topgrossingapplications --region <region> --limit 25 > "$WORK/charts-<region>-grossing.json"
```
(Top-grossing = monetization signal; top-free = demand signal. Add
`toppaidapplications` if willingness-to-pay matters for the angle.) If a fetch
fails, note it and continue with the feeds you got.
Trend signal — use WebSearch for the chosen categories/niches: new releases,
ProductHunt launches, Reddit/news chatter in the last ~90 days, "fastest growing
<category> apps 2026", dated-incumbent complaints. Vary the queries by the
run's angles so two runs don't search the same terms. These results are the
qualitative half the charts can't give.
## Phase 2: Synthesize candidates
Cluster the chart entries + web findings into **at least 12** distinct app/game
ideas (synthesize more than 10 so dedup in Phase 4 still leaves ≥10). For each:
name, category, what-it-does, why-now (trend signal), incumbent(s), monetization
model. Note that App Store `bundle_id`s from the charts are iOS — treat them as
signal, not Android packages. Write the working list as a JSON array (objects
with at least `name`, optional `package`, `category`) to `$WORK/candidates.json`.
## Phase 3: Score
Read `${CLAUDE_PLUGIN_ROOT}/skills/market-research/references/scoring-guide.md`.
Score every candidate's four components and weighted total. Add the subscores and
`total` to each object in `$WORK/candidates.json`. Rank by `total` descending.
## Phase 4: History dedup
Drop anything already suggested:
```bash
python3 ${CLAUDE_PLUGIN_ROOT}/skills/market-research/scripts/history.py \
filter --history "$WORK/history.json" < "$WORK/candidates.json" > "$WORK/fresh.json"
```
If fewer than 10 candidates survive, go back to Phase 1/2 with a different angle
and synthesize more, then re-filter — never present a padded or repeated list.
## Phase 5: Present + handoff
Read `${CLAUDE_PLUGIN_ROOT}/skills/market-research/references/report-template.md`.
Fill it from `$WORK/fresh.json` and write `$WORK/research-<YYYY-MM-DD>.md` (use
the actual run date). Show the user the ranked table (≥10 rows) and your top-3
recommended picks.
Record this run's suggestions so they won't repeat:
```bash
python3 ${CLAUDE_PLUGIN_ROOT}/skills/market-research/scripts/history.py \
add --history "$WORK/history.json" --date <YYYY-MM-DD> --run-id "<RUN_ID>" \
< "$WORK/fresh.json"
```
Then ask which candidate(s) to pursue. For each pick:
1. Resolve it to a Google Play package/URL. If you don't already have the package,
WebSearch `"<name>" site:play.google.com` (or the developer + app name) and
confirm the `play.google.com/store/apps/details?id=...` URL.
2. Invoke the `clone-app` skill on that URL/package to run full feasibility.
If the user picks nothing, stop — the report stands on its own.
## Error Handling Summary
| Scenario | Action |
|---|---|
| `fetch-charts.py` fails for a region | note it, continue with other feeds/web search |
| Web search returns thin results | broaden queries within the chosen angle, try another region |
| < 10 candidates survive dedup | loop back to Phase 1/2 with a new angle, re-filter |
| history.json missing/first run | treat as empty; all candidates are fresh |
| Candidate has no resolvable Play package | skip the handoff for it, keep it in the report as iOS-only/unresolved |
| User picks nothing | stop after writing the report |

View File

@ -0,0 +1,44 @@
# Market Research Report Template
Fill every section. Write to `./work/market-research/research-<YYYY-MM-DD>.md`.
---
# Market Research — <YYYY-MM-DD>
## Run parameters
- Angles this run: <categories / regions / niche lens chosen in Phase 0>
- Focus argument: <the user's focus, or "none">
- Sources: App Store RSS (<feeds/regions pulled>) + web search (<themes>)
- Candidates after history exclusion: <N> (history had <M> prior suggestions)
## Top candidates (ranked)
| # | Name | Category | Clone | Market | Monet. | Niche | **Total** | Why now |
|---|------|----------|------:|-------:|-------:|------:|----------:|---------|
| 1 | … | … | 85 | 80 | 75 | 60 | **79** | one-line trend rationale |
| … | | | | | | | | |
(At least 10 rows.)
## Candidate detail
For each of the top candidates:
### <name>
- **Package (if known):** <com.x.y or "to resolve">
- **What it does:** <12 sentences>
- **Why now:** <trend signal: chart movement, news, dated incumbent>
- **Incumbents:** <who already does this and how weak/strong>
- **Monetization:** <ads / IAP / subscription; ARPU note>
- **Scores:** clone <>, market <>, monetization <>, niche <> → **total <>**
- **Clone risk flags:** <heavy ML / native / content moat / none>
## Recommended picks
Top 3 to send to clone-app first, with one sentence each on why they lead.
## Next step
The user picks one or more candidates; each chosen candidate is resolved to a
Google Play package/URL and handed to the `clone-app` skill for full feasibility.

View File

@ -0,0 +1,51 @@
# Research Angles
A rotating menu of search angles. Each run picks a DIFFERENT combination so
results vary run-to-run (paired with history exclusion). Do not use the same
combination two runs in a row — vary at least the category and the region.
## How to rotate
1. Read `./work/market-research/history.json` if present; note the angles used
recently (the `run_id` encodes the angle — see SKILL Phase 0).
2. Pick **23 categories**, **12 regions**, and **1 niche lens** you did NOT use
last run. If a focus argument was passed to the command, force one category to
match it.
3. Combine into concrete searches in Phase 1.
## Categories
- Hyper-casual games
- Puzzle / word games
- Productivity & utilities
- Finance / fintech
- Health & fitness
- Education / kids
- Photo & video editing
- AI tools (chat, image, voice)
- Social & community
- Lifestyle / habit
## Regions (App Store RSS region codes)
- `us` (United States)
- `gb` (United Kingdom)
- `br` (Brazil — LATAM signal)
- `in` (India)
- `tr` (Türkiye)
- `id` (Indonesia)
- `de` (Germany)
## Niche lenses
- Underserved language/region (few quality localized apps)
- Dated incumbent (top app last updated > 1 year ago)
- Single-feature breakout (one job done very well)
- Rising trend (news/ProductHunt/Reddit chatter in the last ~90 days)
- Monetization mismatch (popular but weakly monetized → headroom)
## App Store RSS feeds to pull (via fetch-charts.py)
- `topfreeapplications` — demand/popularity signal
- `topgrossingapplications` — monetization signal
- `toppaidapplications` — willingness-to-pay signal

View File

@ -0,0 +1,43 @@
# Scoring Guide
Score every candidate 0100 as a weighted composite. Three PRIMARY components
(cloneability, market opportunity, monetization fit) plus a secondary tiebreaker
(niche gap). Show the component scores, not just the total, so picks are auditable.
## Components & weights
| Component | Weight | What it measures |
|---|---|---|
| Cloneability | 35% | How cheaply this rebuilds with clone-app + AI. |
| Market opportunity | 35% | Demand, growth, and incumbent weakness. |
| Monetization fit | 20% | Ads/IAP friendliness and category ARPU. |
| Niche gap (tiebreaker) | 10% | Underserved region/language/segment. |
Total = 0.35·clone + 0.35·market + 0.20·monetization + 0.10·niche, each subscore 0100.
## Scoring each component (0100)
**Cloneability** — higher = easier:
- 80100: simple CRUD/utility, few backend endpoints, no heavy ML, standard UI.
- 5079: moderate backend, some real-time or media, mainstream third-party SDKs.
- 049: heavy ML/on-device models, complex real-time/multiplayer, deep native, large content moat.
**Market opportunity** — higher = better:
- 80100: strong/growing demand, dated or weak incumbents, clear unmet need.
- 5079: healthy demand, beatable incumbents.
- 049: saturated, dominated by entrenched well-funded players.
**Monetization fit** — higher = better:
- 80100: category with proven ads+IAP and high ARPU (casual games, utilities).
- 5079: monetizable but moderate ARPU.
- 049: hard to monetize / users expect free.
**Niche gap** — higher = more underserved:
- 80100: clear language/region/segment with no quality option.
- 049: well served everywhere.
## Output per candidate
For each candidate keep: `name`, `package` (if resolved), `category`, the four
subscores, the weighted `total`, and a one-line rationale. Rank by `total`
descending. Produce at least 10 candidates AFTER history exclusion.

View File

@ -0,0 +1,92 @@
#!/usr/bin/env python3
"""Fetch an Apple App Store RSS chart feed into a normalized JSON list.
Apple publishes public, no-auth RSS chart feeds as JSON at
https://itunes.apple.com/<region>/rss/<feed>/limit=<n>/json . They give a
ranked list of trending apps (name, developer, category, iOS bundle id, price).
This is iOS chart data bundle ids are iOS bundle ids, NOT Android packages.
The market-research skill uses these as trend signal and resolves a Google Play
package later (Phase 5) before any clone-app handoff. Stdlib-only, no pip.
"""
import sys, json, argparse, urllib.request, ssl, subprocess, shutil
FEEDS = ("topfreeapplications", "toppaidapplications", "topgrossingapplications")
UA = "Mozilla/5.0"
def _http_get(url):
"""GET a URL as text. urllib first; on an SSL trust failure (macOS system
Python ships without a CA bundle) fall back to system `curl`."""
req = urllib.request.Request(url, headers={"User-Agent": UA})
try:
with urllib.request.urlopen(req, timeout=30) as r:
return r.read().decode("utf-8", "replace")
except urllib.error.URLError as e:
if not isinstance(e.reason, ssl.SSLError):
raise
if not shutil.which("curl"):
raise
out = subprocess.run(["curl", "-sL", "--fail", "-A", UA, url],
capture_output=True, timeout=60)
if out.returncode != 0:
raise
return out.stdout.decode("utf-8", "replace")
def fetch(feed, region, limit):
url = f"https://itunes.apple.com/{region}/rss/{feed}/limit={limit}/json"
return json.loads(_http_get(url))
def _label(node):
"""Apple wraps text values as {"label": "..."}; return the label or None."""
if isinstance(node, dict):
return node.get("label")
return None
def normalize(data):
entries = []
feed = data.get("feed") or {}
raw = feed.get("entry") or []
if isinstance(raw, dict): # Apple collapses a single entry to a dict
raw = [raw]
for i, e in enumerate(raw, start=1):
cat = (e.get("category") or {}).get("attributes") or {}
idattr = (e.get("id") or {}).get("attributes") or {}
price = (e.get("im:price") or {}).get("attributes") or {}
entries.append({
"rank": i,
"name": _label(e.get("im:name")),
"developer": _label(e.get("im:artist")),
"category": cat.get("label"),
"bundle_id": idattr.get("im:bundleId"),
"price": price.get("amount"),
})
return entries
def main():
ap = argparse.ArgumentParser()
ap.add_argument("feed", choices=FEEDS)
ap.add_argument("--region", default="us")
ap.add_argument("--limit", type=int, default=25)
ap.add_argument("--json-file")
args = ap.parse_args()
if args.json_file:
with open(args.json_file, encoding="utf-8") as f:
data = json.load(f)
else:
try:
data = fetch(args.feed, args.region, args.limit)
except Exception as e:
print(f"ERROR: failed to fetch RSS feed: {e}", file=sys.stderr)
sys.exit(1)
entries = normalize(data)
print(json.dumps({
"feed": args.feed,
"region": args.region,
"count": len(entries),
"entries": entries,
}, indent=2))
if __name__ == "__main__":
main()

View File

@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""Non-repeat memory for market-research suggestions.
history.json holds every candidate ever suggested so a run can avoid repeating
itself. Identity key = package if present, else the lowercased/stripped name.
Subcommands (candidates read as a JSON array on stdin):
filter --history H.json -> print the candidates minus already-seen ones
add --history H.json -> append candidates to H.json, print {added,total}
Date/run_id for added entries come from --date / --run-id (the skill passes the
run date); both default to empty strings so the script is deterministic and
needs no clock. Stdlib-only, no pip.
"""
import sys, json, argparse, os
def cand_key(entry):
"""Canonical identity for a candidate. Package wins; else lowercased name."""
pkg = entry.get("package")
if pkg:
return str(pkg).strip()
return str(entry.get("name", "")).strip().lower()
def load_history(path):
if not os.path.exists(path):
return {"suggestions": []}
with open(path, encoding="utf-8") as f:
data = json.load(f)
data.setdefault("suggestions", [])
return data
def seen_keys(history):
return {s.get("key") for s in history["suggestions"]}
def read_candidates():
data = json.load(sys.stdin)
if not isinstance(data, list):
raise ValueError("candidates stdin must be a JSON array")
return data
def cmd_filter(args):
history = load_history(args.history)
seen = seen_keys(history)
cands = read_candidates()
survivors = [c for c in cands if cand_key(c) not in seen]
print(json.dumps(survivors, indent=2))
def cmd_add(args):
history = load_history(args.history)
seen = seen_keys(history)
cands = read_candidates()
added = 0
for c in cands:
k = cand_key(c)
if k in seen:
continue
history["suggestions"].append({
"key": k,
"name": c.get("name"),
"package": c.get("package"),
"date": args.date,
"run_id": args.run_id,
})
seen.add(k)
added += 1
os.makedirs(os.path.dirname(os.path.abspath(args.history)), exist_ok=True)
with open(args.history, "w", encoding="utf-8") as f:
json.dump(history, f, indent=2)
print(json.dumps({"added": added, "total": len(history["suggestions"])}, indent=2))
def main():
ap = argparse.ArgumentParser()
sub = ap.add_subparsers(dest="cmd", required=True)
for name in ("filter", "add"):
p = sub.add_parser(name)
p.add_argument("--history", required=True)
p.add_argument("--date", default="")
p.add_argument("--run-id", default="")
args = ap.parse_args()
{"filter": cmd_filter, "add": cmd_add}[args.cmd](args)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,5 @@
[
{ "name": "Puzzle Quest Saga", "package": "com.casual.puzzlequest", "category": "Games" },
{ "name": "habit tracker PRO", "category": "Productivity" },
{ "name": "Budget Buddy", "package": "com.fintech.budgetbuddy", "category": "Finance" }
]

View File

@ -0,0 +1,6 @@
{
"suggestions": [
{ "key": "com.casual.puzzlequest", "name": "Puzzle Quest Saga", "package": "com.casual.puzzlequest", "date": "2026-06-01", "run_id": "seed" },
{ "key": "habit tracker pro", "name": "Habit Tracker Pro", "package": null, "date": "2026-06-01", "run_id": "seed" }
]
}

View File

@ -0,0 +1,20 @@
{
"feed": {
"entry": [
{
"im:name": { "label": "Puzzle Quest Saga" },
"im:artist": { "label": "Casual Studio" },
"category": { "attributes": { "label": "Games", "im:id": "6014" } },
"id": { "attributes": { "im:id": "1111111111", "im:bundleId": "com.casual.puzzlequest" } },
"im:price": { "attributes": { "amount": "0.00000", "currency": "USD" } }
},
{
"im:name": { "label": "Budget Buddy" },
"im:artist": { "label": "Fintech Labs" },
"category": { "attributes": { "label": "Finance", "im:id": "6015" } },
"id": { "attributes": { "im:id": "2222222222", "im:bundleId": "com.fintech.budgetbuddy" } },
"im:price": { "attributes": { "amount": "0.00000", "currency": "USD" } }
}
]
}
}

View File

@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -uo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
fail=0
echo "=== structure ==="
bash "$HERE/smoke-structure.sh" || fail=1
echo "=== bash tests ==="
for t in "$HERE"/test-*.sh; do
echo "--- $(basename "$t") ---"
bash "$t" || fail=1
done
echo "=== python tests ==="
for t in "$HERE"/test-*.py; do
echo "--- $(basename "$t") ---"
python3 "$t" || fail=1
done
echo
if [[ "$fail" -eq 0 ]]; then echo "ALL TESTS PASSED"; else echo "SOME TESTS FAILED"; fi
exit $fail

View File

@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -uo pipefail
ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" # repo root
P="$ROOT/plugins/market-research"
fail=0
must_exist() { [[ -e "$1" ]] && echo "PASS exists: ${1#$ROOT/}" || { echo "FAIL missing: ${1#$ROOT/}"; fail=1; }; }
must_exec() { [[ -x "$1" ]] && echo "PASS exec: ${1#$ROOT/}" || { echo "FAIL not exec: ${1#$ROOT/}"; fail=1; }; }
must_exist "$P/.claude-plugin/plugin.json"
must_exist "$P/commands/market-research.md"
must_exist "$P/README.md"
must_exist "$P/skills/market-research/SKILL.md"
for s in fetch-charts.py history.py; do
must_exist "$P/skills/market-research/scripts/$s"
must_exec "$P/skills/market-research/scripts/$s"
done
for r in research-angles scoring-guide report-template; do
must_exist "$P/skills/market-research/references/$r.md"
done
# JSON validity: plugin manifest + marketplace
python3 -c "import json;json.load(open('$P/.claude-plugin/plugin.json'));json.load(open('$ROOT/.claude-plugin/marketplace.json'))" \
&& echo "PASS json valid" || { echo "FAIL json invalid"; fail=1; }
# all three plugins present in marketplace
python3 -c "
import json;d=json.load(open('$ROOT/.claude-plugin/marketplace.json'))
names=[p['name'] for p in d['plugins']]
assert 'market-research' in names and 'clone-app' in names and 'android-reverse-engineering' in names
print('PASS marketplace has all three plugins')" || { echo "FAIL marketplace entries"; fail=1; }
exit $fail

View File

@ -0,0 +1,38 @@
#!/usr/bin/env python3
import json, subprocess, sys, os
HERE = os.path.dirname(os.path.abspath(__file__))
SCRIPT = os.path.join(HERE, "..", "skills", "market-research", "scripts", "fetch-charts.py")
FIXTURE = os.path.join(HERE, "fixtures", "rss-sample.json")
def run():
out = subprocess.check_output(
[sys.executable, SCRIPT, "topfreeapplications",
"--region", "us", "--json-file", FIXTURE])
return json.loads(out)
def main():
d = run()
fails = []
def check(name, cond):
print(f"{'PASS' if cond else 'FAIL'}: {name}")
if not cond: fails.append(name)
check("feed", d["feed"] == "topfreeapplications")
check("region", d["region"] == "us")
check("count", d["count"] == 2)
e0 = d["entries"][0]
check("rank 1", e0["rank"] == 1)
check("name", e0["name"] == "Puzzle Quest Saga")
check("developer", e0["developer"] == "Casual Studio")
check("category", e0["category"] == "Games")
check("bundle_id", e0["bundle_id"] == "com.casual.puzzlequest")
check("price", e0["price"] == "0.00000")
e1 = d["entries"][1]
check("rank 2", e1["rank"] == 2)
check("second name", e1["name"] == "Budget Buddy")
for k in ["rank", "name", "developer", "category", "bundle_id", "price"]:
check(f"key present: {k}", k in e0)
sys.exit(1 if fails else 0)
main()

View File

@ -0,0 +1,51 @@
#!/usr/bin/env python3
import json, subprocess, sys, os, tempfile, shutil
HERE = os.path.dirname(os.path.abspath(__file__))
SCRIPT = os.path.join(HERE, "..", "skills", "market-research", "scripts", "history.py")
FIX = os.path.join(HERE, "fixtures")
def run(args, stdin_path):
with open(stdin_path, "rb") as f:
return subprocess.run([sys.executable, SCRIPT, *args],
stdin=f, capture_output=True)
def main():
fails = []
def check(name, cond):
print(f"{'PASS' if cond else 'FAIL'}: {name}")
if not cond: fails.append(name)
cands = os.path.join(FIX, "candidates-sample.json")
# filter: against the seed, only "Budget Buddy" survives
r = run(["filter", "--history", os.path.join(FIX, "history-seed.json")], cands)
check("filter exit 0", r.returncode == 0)
survivors = json.loads(r.stdout)
names = sorted(c["name"] for c in survivors)
check("filter drops package collision + name collision", names == ["Budget Buddy"])
# filter against a missing history file = nothing seen, all 3 survive
tmp = tempfile.mkdtemp()
try:
missing = os.path.join(tmp, "nope.json")
r = run(["filter", "--history", missing], cands)
check("filter missing-history exit 0", r.returncode == 0)
check("filter missing-history keeps all", len(json.loads(r.stdout)) == 3)
# add: appends all 3 to a fresh history, reports counts
h = os.path.join(tmp, "h.json")
r = run(["add", "--history", h], cands)
check("add exit 0", r.returncode == 0)
rep = json.loads(r.stdout)
check("add reports added 3", rep["added"] == 3)
check("add reports total 3", rep["total"] == 3)
saved = json.load(open(h))
check("history file has 3 suggestions", len(saved["suggestions"]) == 3)
check("history entries carry key", all("key" in s for s in saved["suggestions"]))
finally:
shutil.rmtree(tmp)
sys.exit(1 if fails else 0)
main()

View File

@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -uo pipefail
ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
SKILL="$ROOT/plugins/market-research/skills/market-research/SKILL.md"
fail=0
has() { grep -q "$1" "$SKILL" && echo "PASS contains: $1" || { echo "FAIL missing: $1"; fail=1; }; }
[[ -f "$SKILL" ]] || { echo "FAIL: SKILL.md missing"; exit 1; }
# frontmatter
has "description:"
has "trigger:"
# all six phases
has "Phase 0"
has "Phase 1"
has "Phase 2"
has "Phase 3"
has "Phase 4"
has "Phase 5"
# wires both scripts and the rubrics
has "fetch-charts.py"
has "history.py"
has "scoring-guide.md"
has "research-angles.md"
has "report-template.md"
# state contract + handoff
has "work/market-research"
has "history.json"
has "clone-app"
exit $fail