From f2411edfd8a02279949c8da9db6a32945cb4101a Mon Sep 17 00:00:00 2001 From: "fatih.bulut" Date: Mon, 22 Jun 2026 00:12:35 +0300 Subject: [PATCH 1/9] docs(market-research): add market-to-build pipeline design spec Umbrella spec for two new plugins (market-research, hermes-build) plus a one-branch clone-app Phase 7 edit, chaining discover -> analyze -> build. Decomposes into three implementation plans built in order. Co-Authored-By: Claude Opus 4.8 --- ...6-06-22-market-to-build-pipeline-design.md | 228 ++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-22-market-to-build-pipeline-design.md diff --git a/docs/superpowers/specs/2026-06-22-market-to-build-pipeline-design.md b/docs/superpowers/specs/2026-06-22-market-to-build-pipeline-design.md new file mode 100644 index 0000000..762dca5 --- /dev/null +++ b/docs/superpowers/specs/2026-06-22-market-to-build-pipeline-design.md @@ -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//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 0–100, + 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-.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//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-.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//hermes/`: bundle files, `mission-brief.md`, `preflight.json`, +`build-report-.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 `; 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). From 1baf142ae5830303e66a9fe5be62c7a9ffb73417 Mon Sep 17 00:00:00 2001 From: "fatih.bulut" Date: Mon, 22 Jun 2026 00:33:57 +0300 Subject: [PATCH 2/9] docs(market-research): add implementation plan Six-task TDD plan for the market-research plugin: scaffold + marketplace entry, fetch-charts.py (App Store RSS), history.py (non-repeat memory), reference rubrics, 6-phase SKILL.md with clone-app handoff, test aggregator. Co-Authored-By: Claude Opus 4.8 --- .../plans/2026-06-22-market-research.md | 1121 +++++++++++++++++ 1 file changed, 1121 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-22-market-research.md diff --git a/docs/superpowers/plans/2026-06-22-market-research.md b/docs/superpowers/plans/2026-06-22-market-research.md new file mode 100644 index 0000000..a21df36 --- /dev/null +++ b/docs/superpowers/plans/2026-06-22-market-research.md @@ -0,0 +1,1121 @@ +# market-research Plugin Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build a new `market-research` Claude Code plugin whose `/market-research` skill autonomously scans the app/game market (free web + LLM trend synthesis), produces ≥10 scored, non-repeating clone candidates, and hands each user-chosen candidate to the existing `clone-app` skill. + +**Architecture:** A phased prose SKILL.md orchestrator (same shape as `clone-app/SKILL.md`) backed by two stdlib-only Python helper scripts (`fetch-charts.py` for App Store RSS chart data, `history.py` for the non-repeat memory) and three Markdown reference rubrics (`research-angles`, `scoring-guide`, `report-template`). Each script is independently testable offline against `tests/fixtures/`. The plugin is self-contained under `plugins/market-research/`; the only shared-file edit is adding one entry to root `.claude-plugin/marketplace.json`. + +**Tech Stack:** Bash (`#!/usr/bin/env bash`), Python 3 stdlib only (`urllib`, `json`, `argparse`, `ssl`, `subprocess`, `shutil`), Markdown skill/command/reference docs. No pip, no virtualenv. + +## Global Constraints + +- **Never modify `plugins/android-reverse-engineering/`** — `git status --porcelain plugins/android-reverse-engineering/` must print nothing before any commit. +- **Python is stdlib-only** — `urllib`, `json`, `re`, `argparse`, `ssl`, `subprocess`, `shutil`. No pip, no virtualenv. +- **All scripts** use `#!/usr/bin/env bash` or `#!/usr/bin/env python3`; bash scripts are invoked with `bash `, not `sh`. +- **Bash tests** use `set -uo pipefail` (not `-e`), aggregate failures into a `fail` var so every assertion runs, and `exit $fail`. +- **Python network scripts** must support an offline flag (`--json-file`) and be tested against `tests/fixtures/` — never hitting the network in tests. +- **Working dir** at runtime is `./work/market-research/` relative to the user's cwd — never inside the plugin. +- **Effort** is measured in "AI Sprints" (one focused Claude session), never calendar time. +- **Commits** follow Conventional Commits scoped to the plugin: `feat(market-research): …`, `test(market-research): …`. +- **HTTP GETs** copy the existing clone-app pattern: try `urllib` with `User-Agent: "Mozilla/5.0"`, fall back to system `curl` on an `ssl.SSLError` (macOS system Python ships without a CA bundle). +- **Candidate identity key** is the package if present, else the lowercased, stripped name. This key is used identically by `history.py` dedup and by the SKILL's handoff. Defined in Task 3, reused in Task 5. + +--- + +### Task 1: Plugin scaffold + structural smoke test + +Create the plugin skeleton (manifest, command, README), register it in the marketplace, and add a structural smoke test that locks the file layout in place. Later tasks fill the scripts/references/skill this test expects, so the smoke test is written now but its script/reference/skill assertions will only fully pass once Tasks 2–5 land — Step 4 below runs only the subset that exists after this task. + +**Files:** +- Create: `plugins/market-research/.claude-plugin/plugin.json` +- Create: `plugins/market-research/commands/market-research.md` +- Create: `plugins/market-research/README.md` +- Create: `plugins/market-research/tests/smoke-structure.sh` +- Modify: `.claude-plugin/marketplace.json` (append one plugin entry) + +**Interfaces:** +- Consumes: nothing (first task). +- Produces: the directory layout `plugins/market-research/{.claude-plugin,commands,skills/market-research/{scripts,references},tests/fixtures}` that all later tasks write into; the marketplace entry `name: "market-research"`. + +- [ ] **Step 1: Write the plugin manifest** + +Create `plugins/market-research/.claude-plugin/plugin.json`: + +```json +{ + "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/" +} +``` + +- [ ] **Step 2: Write the slash command** + +Create `plugins/market-research/commands/market-research.md`: + +```markdown +--- +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-.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. +``` + +- [ ] **Step 3: Write the README** + +Create `plugins/market-research/README.md`: + +```markdown +# 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-.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. +``` + +- [ ] **Step 4: Append the marketplace entry** + +In `.claude-plugin/marketplace.json`, add a third object to the `plugins` array, after the `clone-app` entry (keep the existing two entries byte-identical). The new entry: + +```json +{ + "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" +} +``` + +- [ ] **Step 5: Write the structural smoke test** + +Create `plugins/market-research/tests/smoke-structure.sh`. It checks the full intended layout (including files created in Tasks 2–5) so it doubles as the plugin's final structural gate; everything is asserted now and the later tasks make it fully green. + +```bash +#!/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 +``` + +- [ ] **Step 6: Make the test executable and create empty dirs** + +Run: +```bash +chmod +x plugins/market-research/tests/smoke-structure.sh +mkdir -p plugins/market-research/skills/market-research/scripts \ + plugins/market-research/skills/market-research/references \ + plugins/market-research/tests/fixtures +``` + +- [ ] **Step 7: Verify upstream untouched + marketplace valid** + +Run: +```bash +git status --porcelain plugins/android-reverse-engineering/ +python3 -c "import json; json.load(open('.claude-plugin/marketplace.json')); print('marketplace OK')" +``` +Expected: the `git status` line prints **nothing**; the python prints `marketplace OK`. + +- [ ] **Step 8: Commit** + +```bash +git add plugins/market-research/.claude-plugin plugins/market-research/commands \ + plugins/market-research/README.md plugins/market-research/tests/smoke-structure.sh \ + .claude-plugin/marketplace.json +git commit -m "feat(market-research): scaffold plugin, command, README, marketplace entry" +``` + +--- + +### Task 2: `fetch-charts.py` — App Store RSS chart fetcher + +Fetch and normalize Apple's public App Store RSS chart feeds (top free / top paid / top grossing) into a flat JSON list of ranked entries. This is the only hard chart data the skill ingests; trend signal beyond it comes from the model's web search in the SKILL prose (not scriptable/testable). iOS bundle IDs are informational only — Android package resolution happens later in the skill. + +**Files:** +- Create: `plugins/market-research/skills/market-research/scripts/fetch-charts.py` +- Create: `plugins/market-research/tests/fixtures/rss-sample.json` +- Test: `plugins/market-research/tests/test-fetch-charts.py` + +**Interfaces:** +- Consumes: nothing from earlier tasks. +- Produces: CLI `python3 fetch-charts.py [--region us] [--limit 25] [--json-file PATH]` printing `json.dumps(..., indent=2)` of `{"feed","region","count","entries":[...]}`. Each entry is a dict with keys `rank` (int), `name` (str|None), `developer` (str|None), `category` (str|None), `bundle_id` (str|None), `price` (str|None). `` is one of `topfreeapplications`, `toppaidapplications`, `topgrossingapplications`. + +- [ ] **Step 1: Create the RSS fixture** + +Create `plugins/market-research/tests/fixtures/rss-sample.json` — a trimmed but structurally faithful Apple RSS JSON with two entries: + +```json +{ + "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" } } + } + ] + } +} +``` + +- [ ] **Step 2: Write the failing test** + +Create `plugins/market-research/tests/test-fetch-charts.py`: + +```python +#!/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() +``` + +- [ ] **Step 3: Run the test to verify it fails** + +Run: `python3 plugins/market-research/tests/test-fetch-charts.py` +Expected: FAIL — the script file does not exist yet (subprocess raises / non-zero). + +- [ ] **Step 4: Write the implementation** + +Create `plugins/market-research/skills/market-research/scripts/fetch-charts.py`: + +```python +#!/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//rss//limit=/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() +``` + +- [ ] **Step 5: Make it executable and run the test to verify it passes** + +Run: +```bash +chmod +x plugins/market-research/skills/market-research/scripts/fetch-charts.py +python3 plugins/market-research/tests/test-fetch-charts.py +``` +Expected: every line `PASS`, exit 0. + +- [ ] **Step 6: Commit** + +```bash +git add plugins/market-research/skills/market-research/scripts/fetch-charts.py \ + plugins/market-research/tests/test-fetch-charts.py \ + plugins/market-research/tests/fixtures/rss-sample.json +git commit -m "feat(market-research): add fetch-charts.py App Store RSS fetcher with offline test" +``` + +--- + +### Task 3: `history.py` — non-repeat suggestion memory + +Maintain `history.json`: the list of every candidate ever suggested, so each run can exclude what it has already proposed. Two subcommands: `filter` (drop already-seen candidates from a list, on stdout) and `add` (append candidates to history). Identity key = package if present, else lowercased/stripped name — the shared key from Global Constraints. + +**Files:** +- Create: `plugins/market-research/skills/market-research/scripts/history.py` +- Create: `plugins/market-research/tests/fixtures/history-seed.json` +- Create: `plugins/market-research/tests/fixtures/candidates-sample.json` +- Test: `plugins/market-research/tests/test-history.py` + +**Interfaces:** +- Consumes: nothing from earlier tasks. +- Produces: + - `python3 history.py filter --history H.json < candidates.json` → prints the candidates array with already-seen entries removed. + - `python3 history.py add --history H.json < candidates.json` → appends candidates to `H.json` (creating it if absent), writes the file, prints `{"added": N, "total": M}`. + - Importable `cand_key(entry) -> str`: returns `entry["package"]` if truthy, else `entry["name"].strip().lower()`. This is the canonical candidate identity used by the SKILL handoff too. + - A candidate is an object with at least a `name`; `package` is optional. + - `history.json` on disk is `{"suggestions": [ {key, name, package, date, run_id}, ... ]}`. + +- [ ] **Step 1: Create the fixtures** + +Create `plugins/market-research/tests/fixtures/history-seed.json`: + +```json +{ + "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" } + ] +} +``` + +Create `plugins/market-research/tests/fixtures/candidates-sample.json` — three candidates, two of which collide with the seed (one by package, one by name-case): + +```json +[ + { "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" } +] +``` + +- [ ] **Step 2: Write the failing test** + +Create `plugins/market-research/tests/test-history.py`: + +```python +#!/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() +``` + +- [ ] **Step 3: Run the test to verify it fails** + +Run: `python3 plugins/market-research/tests/test-history.py` +Expected: FAIL — `history.py` does not exist yet. + +- [ ] **Step 4: Write the implementation** + +Create `plugins/market-research/skills/market-research/scripts/history.py`: + +```python +#!/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() +``` + +- [ ] **Step 5: Make it executable and run the test to verify it passes** + +Run: +```bash +chmod +x plugins/market-research/skills/market-research/scripts/history.py +python3 plugins/market-research/tests/test-history.py +``` +Expected: every line `PASS`, exit 0. + +- [ ] **Step 6: Commit** + +```bash +git add plugins/market-research/skills/market-research/scripts/history.py \ + plugins/market-research/tests/test-history.py \ + plugins/market-research/tests/fixtures/history-seed.json \ + plugins/market-research/tests/fixtures/candidates-sample.json +git commit -m "feat(market-research): add history.py non-repeat memory with offline test" +``` + +--- + +### Task 4: Reference rubrics + +Write the three Markdown rubrics the SKILL reads so AI-judgment steps (angle rotation, scoring, report shape) stay consistent and tunable without editing the prose. These are documentation deliverables — no code test beyond the smoke test's existence check (Task 1, Step 5), so this task's gate is that smoke-structure passes their `must_exist` lines. + +**Files:** +- Create: `plugins/market-research/skills/market-research/references/research-angles.md` +- Create: `plugins/market-research/skills/market-research/references/scoring-guide.md` +- Create: `plugins/market-research/skills/market-research/references/report-template.md` + +**Interfaces:** +- Consumes: the candidate identity concept from Task 3 (scoring-guide references the same `name`/`package` fields). +- Produces: three rubric files the SKILL (Task 5) reads by path. `scoring-guide.md` defines the four score components and their weights that the SKILL's Phase 3 applies. `report-template.md` defines the section order the SKILL's Phase 5 fills. + +- [ ] **Step 1: Write the research-angles rubric** + +Create `plugins/market-research/skills/market-research/references/research-angles.md`: + +```markdown +# 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 **2–3 categories**, **1–2 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 +``` + +- [ ] **Step 2: Write the scoring rubric** + +Create `plugins/market-research/skills/market-research/references/scoring-guide.md`: + +```markdown +# Scoring Guide + +Score every candidate 0–100 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 0–100. + +## Scoring each component (0–100) + +**Cloneability** — higher = easier: +- 80–100: simple CRUD/utility, few backend endpoints, no heavy ML, standard UI. +- 50–79: moderate backend, some real-time or media, mainstream third-party SDKs. +- 0–49: heavy ML/on-device models, complex real-time/multiplayer, deep native, large content moat. + +**Market opportunity** — higher = better: +- 80–100: strong/growing demand, dated or weak incumbents, clear unmet need. +- 50–79: healthy demand, beatable incumbents. +- 0–49: saturated, dominated by entrenched well-funded players. + +**Monetization fit** — higher = better: +- 80–100: category with proven ads+IAP and high ARPU (casual games, utilities). +- 50–79: monetizable but moderate ARPU. +- 0–49: hard to monetize / users expect free. + +**Niche gap** — higher = more underserved: +- 80–100: clear language/region/segment with no quality option. +- 0–49: 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. +``` + +- [ ] **Step 3: Write the report template** + +Create `plugins/market-research/skills/market-research/references/report-template.md`: + +```markdown +# Market Research Report Template + +Fill every section. Write to `./work/market-research/research-.md`. + +--- + +# Market Research — + +## Run parameters +- Angles this run: +- Focus argument: +- Sources: App Store RSS () + web search () +- Candidates after history exclusion: (history had 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: + +### +- **Package (if known):** +- **What it does:** <1–2 sentences> +- **Why now:** +- **Incumbents:** +- **Monetization:** +- **Scores:** clone <>, market <>, monetization <>, niche <> → **total <>** +- **Clone risk flags:** + +## 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. +``` + +- [ ] **Step 4: Run the structural smoke test (references now exist)** + +Run: `bash plugins/market-research/tests/smoke-structure.sh` +Expected: the three `references/*.md` lines now print `PASS exists`. (SKILL.md and the scripts may still show their state per which tasks have run; if Tasks 2–3 are already done, only the SKILL line is still expected to fail until Task 5.) + +- [ ] **Step 5: Commit** + +```bash +git add plugins/market-research/skills/market-research/references/ +git commit -m "feat(market-research): add research-angles, scoring, and report rubrics" +``` + +--- + +### Task 5: SKILL.md orchestrator + +Write the phased prose workflow Claude executes — the heart of the plugin. It wires the scripts and rubrics into the 6-phase flow and ends by handing user-chosen candidates to `clone-app`. No new code test; its gate is the smoke test (SKILL.md existence) plus a content grep for the phase markers and the clone-app handoff. + +**Files:** +- Create: `plugins/market-research/skills/market-research/SKILL.md` +- Test: `plugins/market-research/tests/test-skill-content.sh` + +**Interfaces:** +- Consumes: `fetch-charts.py` CLI (Task 2), `history.py filter`/`add` CLI and the `cand_key` rule (Task 3), the three rubrics (Task 4). +- Produces: the executable workflow. Phase 5 invokes the `clone-app` skill per pick. Defines the on-disk state contract: `./work/market-research/history.json` and `./work/market-research/research-.md`. + +- [ ] **Step 1: Write the failing content test** + +Create `plugins/market-research/tests/test-skill-content.sh`: + +```bash +#!/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 +``` + +- [ ] **Step 2: Run the test to verify it fails** + +Run: `bash plugins/market-research/tests/test-skill-content.sh` +Expected: FAIL on the first line — `SKILL.md missing`. + +- [ ] **Step 3: Write the SKILL.md** + +Create `plugins/market-research/skills/market-research/SKILL.md`: + +```markdown +--- +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 (0–5). 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-.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 2–3 categories, 1–2 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 --limit 25 > "$WORK/charts--free.json" +python3 ${CLAUDE_PLUGIN_ROOT}/skills/market-research/scripts/fetch-charts.py \ + topgrossingapplications --region --limit 25 > "$WORK/charts--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 + 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-.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 --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 `"" 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 | +``` + +- [ ] **Step 4: Run the content test to verify it passes** + +Run: `bash plugins/market-research/tests/test-skill-content.sh` +Expected: every line `PASS`, exit 0. + +- [ ] **Step 5: Run the structural smoke test (now fully green)** + +Run: `bash plugins/market-research/tests/smoke-structure.sh` +Expected: every line `PASS`, exit 0. + +- [ ] **Step 6: Commit** + +```bash +git add plugins/market-research/skills/market-research/SKILL.md \ + plugins/market-research/tests/test-skill-content.sh +git commit -m "feat(market-research): add 6-phase SKILL.md orchestrator with clone-app handoff" +``` + +--- + +### Task 6: Test aggregator + full-suite green + +Add the `run-all.sh` aggregator (mirroring clone-app's) so the whole plugin tests with one command, and confirm everything passes together with the upstream tree still untouched. + +**Files:** +- Create: `plugins/market-research/tests/run-all.sh` + +**Interfaces:** +- Consumes: every `test-*.sh` and `test-*.py` plus `smoke-structure.sh` from Tasks 1–5. +- Produces: `bash plugins/market-research/tests/run-all.sh` → runs the suite, prints `ALL TESTS PASSED` / `SOME TESTS FAILED`, exits non-zero on any failure. + +- [ ] **Step 1: Write the aggregator** + +Create `plugins/market-research/tests/run-all.sh`: + +```bash +#!/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 +``` + +- [ ] **Step 2: Make it executable and run the full suite** + +Run: +```bash +chmod +x plugins/market-research/tests/run-all.sh +bash plugins/market-research/tests/run-all.sh +``` +Expected: ends with `ALL TESTS PASSED`, exit 0. + +- [ ] **Step 3: Verify the upstream tree is still byte-identical** + +Run: `git status --porcelain plugins/android-reverse-engineering/` +Expected: prints **nothing**. + +- [ ] **Step 4: Verify both manifests are valid JSON** + +Run: +```bash +python3 -c "import json; json.load(open('.claude-plugin/marketplace.json')); json.load(open('plugins/market-research/.claude-plugin/plugin.json')); print('JSON OK')" +``` +Expected: `JSON OK`. + +- [ ] **Step 5: Commit** + +```bash +git add plugins/market-research/tests/run-all.sh +git commit -m "test(market-research): add run-all.sh suite aggregator" +``` + +--- + +## Self-Review + +**1. Spec coverage** (against §6 of the design spec): +- P0 seed rotation → Task 4 `research-angles.md` + Task 5 SKILL Phase 0. ✓ +- P1 gather (App Store RSS + web) → Task 2 `fetch-charts.py` + SKILL Phase 1. ✓ +- P2 synthesize ≥10 → SKILL Phase 2 (synthesize ≥12 to survive dedup). ✓ +- P3 composite score → Task 4 `scoring-guide.md` + SKILL Phase 3. ✓ +- P4 history dedup → Task 3 `history.py` + SKILL Phase 4. ✓ +- P5 present + clone-app handoff incl. package resolution → SKILL Phase 5. ✓ +- State files (`history.json`, `research-.md`) → Task 3 + SKILL state section. ✓ +- Scripts stdlib-only, offline-fixture tested → Tasks 2,3. ✓ +- Three rubrics → Task 4. ✓ +- Tests (fixtures, dedup, scoring sanity via scoring-guide, smoke) → Tasks 1,2,3,5,6. ✓ +- Marketplace entry, own plugin.json → Task 1. ✓ +- Upstream untouched → checked in Tasks 1,6. ✓ + +Note: the spec mentioned a "scoring sanity" test; scoring is an AI-judgment step driven by `scoring-guide.md` (a rubric, not code), so it is validated by the SKILL content test referencing the rubric rather than a unit test — consistent with clone-app, where rubric-driven steps have no unit test. + +**2. Placeholder scan:** No "TBD"/"TODO"/"handle edge cases" left. All code blocks are complete and runnable. Error handling is concrete (explicit fallbacks in scripts and the SKILL Error Handling table). + +**3. Type consistency:** `cand_key` defined once (Task 3) and referenced by name in Task 5; candidate object shape (`name`, optional `package`, `category`, subscores, `total`) is consistent across Tasks 3, 4, 5. `fetch-charts.py` output keys (`feed`,`region`,`count`,`entries[]` with `rank`/`name`/`developer`/`category`/`bundle_id`/`price`) match between Task 2 implementation and test. `history.py` subcommands (`filter`/`add`, `--history`/`--date`/`--run-id`) match between Task 3 impl, test, and SKILL Phase 4/5 calls. From 31502b5bbebba6eb3f33b4457e19fb85916a1dba Mon Sep 17 00:00:00 2001 From: "fatih.bulut" Date: Mon, 22 Jun 2026 00:46:09 +0300 Subject: [PATCH 3/9] feat(market-research): scaffold plugin, command, README, marketplace entry --- .claude-plugin/marketplace.json | 13 +++++++ .../.claude-plugin/plugin.json | 13 +++++++ plugins/market-research/README.md | 37 +++++++++++++++++++ .../commands/market-research.md | 30 +++++++++++++++ .../market-research/tests/smoke-structure.sh | 32 ++++++++++++++++ 5 files changed, 125 insertions(+) create mode 100644 plugins/market-research/.claude-plugin/plugin.json create mode 100644 plugins/market-research/README.md create mode 100644 plugins/market-research/commands/market-research.md create mode 100755 plugins/market-research/tests/smoke-structure.sh diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 5af5e33..59e27a3 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -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" } ] } diff --git a/plugins/market-research/.claude-plugin/plugin.json b/plugins/market-research/.claude-plugin/plugin.json new file mode 100644 index 0000000..655644f --- /dev/null +++ b/plugins/market-research/.claude-plugin/plugin.json @@ -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/" +} diff --git a/plugins/market-research/README.md b/plugins/market-research/README.md new file mode 100644 index 0000000..8c8ccbb --- /dev/null +++ b/plugins/market-research/README.md @@ -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-.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. diff --git a/plugins/market-research/commands/market-research.md b/plugins/market-research/commands/market-research.md new file mode 100644 index 0000000..45f9202 --- /dev/null +++ b/plugins/market-research/commands/market-research.md @@ -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-.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. diff --git a/plugins/market-research/tests/smoke-structure.sh b/plugins/market-research/tests/smoke-structure.sh new file mode 100755 index 0000000..6fa78ac --- /dev/null +++ b/plugins/market-research/tests/smoke-structure.sh @@ -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 From 5beb04b43d2a25139e9f1f0a798da49a8b492d88 Mon Sep 17 00:00:00 2001 From: "fatih.bulut" Date: Mon, 22 Jun 2026 00:49:57 +0300 Subject: [PATCH 4/9] feat(market-research): add fetch-charts.py App Store RSS fetcher with offline test --- .../market-research/scripts/fetch-charts.py | 92 +++++++++++++++++++ .../tests/fixtures/rss-sample.json | 20 ++++ .../tests/test-fetch-charts.py | 38 ++++++++ 3 files changed, 150 insertions(+) create mode 100755 plugins/market-research/skills/market-research/scripts/fetch-charts.py create mode 100644 plugins/market-research/tests/fixtures/rss-sample.json create mode 100644 plugins/market-research/tests/test-fetch-charts.py diff --git a/plugins/market-research/skills/market-research/scripts/fetch-charts.py b/plugins/market-research/skills/market-research/scripts/fetch-charts.py new file mode 100755 index 0000000..be3aa7a --- /dev/null +++ b/plugins/market-research/skills/market-research/scripts/fetch-charts.py @@ -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//rss//limit=/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() diff --git a/plugins/market-research/tests/fixtures/rss-sample.json b/plugins/market-research/tests/fixtures/rss-sample.json new file mode 100644 index 0000000..78e6ff9 --- /dev/null +++ b/plugins/market-research/tests/fixtures/rss-sample.json @@ -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" } } + } + ] + } +} diff --git a/plugins/market-research/tests/test-fetch-charts.py b/plugins/market-research/tests/test-fetch-charts.py new file mode 100644 index 0000000..bc2e7c4 --- /dev/null +++ b/plugins/market-research/tests/test-fetch-charts.py @@ -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() From ae840beaa325a790edcd5be340697f85d44e9957 Mon Sep 17 00:00:00 2001 From: "fatih.bulut" Date: Mon, 22 Jun 2026 00:52:45 +0300 Subject: [PATCH 5/9] feat(market-research): add history.py non-repeat memory with offline test --- .../skills/market-research/scripts/history.py | 83 +++++++++++++++++++ .../tests/fixtures/candidates-sample.json | 5 ++ .../tests/fixtures/history-seed.json | 6 ++ plugins/market-research/tests/test-history.py | 51 ++++++++++++ 4 files changed, 145 insertions(+) create mode 100755 plugins/market-research/skills/market-research/scripts/history.py create mode 100644 plugins/market-research/tests/fixtures/candidates-sample.json create mode 100644 plugins/market-research/tests/fixtures/history-seed.json create mode 100644 plugins/market-research/tests/test-history.py diff --git a/plugins/market-research/skills/market-research/scripts/history.py b/plugins/market-research/skills/market-research/scripts/history.py new file mode 100755 index 0000000..16ba9b9 --- /dev/null +++ b/plugins/market-research/skills/market-research/scripts/history.py @@ -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() diff --git a/plugins/market-research/tests/fixtures/candidates-sample.json b/plugins/market-research/tests/fixtures/candidates-sample.json new file mode 100644 index 0000000..9e2af50 --- /dev/null +++ b/plugins/market-research/tests/fixtures/candidates-sample.json @@ -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" } +] diff --git a/plugins/market-research/tests/fixtures/history-seed.json b/plugins/market-research/tests/fixtures/history-seed.json new file mode 100644 index 0000000..13db63f --- /dev/null +++ b/plugins/market-research/tests/fixtures/history-seed.json @@ -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" } + ] +} diff --git a/plugins/market-research/tests/test-history.py b/plugins/market-research/tests/test-history.py new file mode 100644 index 0000000..63ad544 --- /dev/null +++ b/plugins/market-research/tests/test-history.py @@ -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() From c18ed6ab0931138cc48b93d5edc0875801e68e71 Mon Sep 17 00:00:00 2001 From: "fatih.bulut" Date: Mon, 22 Jun 2026 00:56:42 +0300 Subject: [PATCH 6/9] feat(market-research): add research-angles, scoring, and report rubrics --- .../references/report-template.md | 44 ++++++++++++++++ .../references/research-angles.md | 51 +++++++++++++++++++ .../references/scoring-guide.md | 43 ++++++++++++++++ 3 files changed, 138 insertions(+) create mode 100644 plugins/market-research/skills/market-research/references/report-template.md create mode 100644 plugins/market-research/skills/market-research/references/research-angles.md create mode 100644 plugins/market-research/skills/market-research/references/scoring-guide.md diff --git a/plugins/market-research/skills/market-research/references/report-template.md b/plugins/market-research/skills/market-research/references/report-template.md new file mode 100644 index 0000000..1ecc68f --- /dev/null +++ b/plugins/market-research/skills/market-research/references/report-template.md @@ -0,0 +1,44 @@ +# Market Research Report Template + +Fill every section. Write to `./work/market-research/research-.md`. + +--- + +# Market Research — + +## Run parameters +- Angles this run: +- Focus argument: +- Sources: App Store RSS () + web search () +- Candidates after history exclusion: (history had 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: + +### +- **Package (if known):** +- **What it does:** <1–2 sentences> +- **Why now:** +- **Incumbents:** +- **Monetization:** +- **Scores:** clone <>, market <>, monetization <>, niche <> → **total <>** +- **Clone risk flags:** + +## 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. diff --git a/plugins/market-research/skills/market-research/references/research-angles.md b/plugins/market-research/skills/market-research/references/research-angles.md new file mode 100644 index 0000000..f2d7ebc --- /dev/null +++ b/plugins/market-research/skills/market-research/references/research-angles.md @@ -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 **2–3 categories**, **1–2 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 diff --git a/plugins/market-research/skills/market-research/references/scoring-guide.md b/plugins/market-research/skills/market-research/references/scoring-guide.md new file mode 100644 index 0000000..df287eb --- /dev/null +++ b/plugins/market-research/skills/market-research/references/scoring-guide.md @@ -0,0 +1,43 @@ +# Scoring Guide + +Score every candidate 0–100 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 0–100. + +## Scoring each component (0–100) + +**Cloneability** — higher = easier: +- 80–100: simple CRUD/utility, few backend endpoints, no heavy ML, standard UI. +- 50–79: moderate backend, some real-time or media, mainstream third-party SDKs. +- 0–49: heavy ML/on-device models, complex real-time/multiplayer, deep native, large content moat. + +**Market opportunity** — higher = better: +- 80–100: strong/growing demand, dated or weak incumbents, clear unmet need. +- 50–79: healthy demand, beatable incumbents. +- 0–49: saturated, dominated by entrenched well-funded players. + +**Monetization fit** — higher = better: +- 80–100: category with proven ads+IAP and high ARPU (casual games, utilities). +- 50–79: monetizable but moderate ARPU. +- 0–49: hard to monetize / users expect free. + +**Niche gap** — higher = more underserved: +- 80–100: clear language/region/segment with no quality option. +- 0–49: 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. From 0da572ca1f1f032debcae7e37997f1bde31cd7b9 Mon Sep 17 00:00:00 2001 From: "fatih.bulut" Date: Mon, 22 Jun 2026 00:59:25 +0300 Subject: [PATCH 7/9] feat(market-research): add 6-phase SKILL.md orchestrator with clone-app handoff --- .../skills/market-research/SKILL.md | 107 ++++++++++++++++++ .../tests/test-skill-content.sh | 29 +++++ 2 files changed, 136 insertions(+) create mode 100644 plugins/market-research/skills/market-research/SKILL.md create mode 100755 plugins/market-research/tests/test-skill-content.sh diff --git a/plugins/market-research/skills/market-research/SKILL.md b/plugins/market-research/skills/market-research/SKILL.md new file mode 100644 index 0000000..70dbd33 --- /dev/null +++ b/plugins/market-research/skills/market-research/SKILL.md @@ -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 (0–5). 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-.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 2–3 categories, 1–2 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 --limit 25 > "$WORK/charts--free.json" +python3 ${CLAUDE_PLUGIN_ROOT}/skills/market-research/scripts/fetch-charts.py \ + topgrossingapplications --region --limit 25 > "$WORK/charts--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 + 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-.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 --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 `"" 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 | diff --git a/plugins/market-research/tests/test-skill-content.sh b/plugins/market-research/tests/test-skill-content.sh new file mode 100755 index 0000000..33cff4b --- /dev/null +++ b/plugins/market-research/tests/test-skill-content.sh @@ -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 From 67ac1e6322d74de35e22d8015e0d297c5e75e29c Mon Sep 17 00:00:00 2001 From: "fatih.bulut" Date: Mon, 22 Jun 2026 01:02:03 +0300 Subject: [PATCH 8/9] test(market-research): add run-all.sh suite aggregator --- plugins/market-research/tests/run-all.sh | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100755 plugins/market-research/tests/run-all.sh diff --git a/plugins/market-research/tests/run-all.sh b/plugins/market-research/tests/run-all.sh new file mode 100755 index 0000000..4cf8ea3 --- /dev/null +++ b/plugins/market-research/tests/run-all.sh @@ -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 From cf96d35f1c860b001eda4215cdd7e132ac2870a7 Mon Sep 17 00:00:00 2001 From: "fatih.bulut" Date: Mon, 22 Jun 2026 01:06:02 +0300 Subject: [PATCH 9/9] chore(market-research): ignore __pycache__ and .pyc artifacts Co-Authored-By: Claude Opus 4.8 --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc