Merge pull request #3 from masa2146/feat/market_research_skill
Feat/market research skill
This commit is contained in:
commit
0c0224d493
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
__pycache__/
|
||||
*.pyc
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,228 @@
|
|||
# Market-to-Build Pipeline — Design Spec
|
||||
|
||||
**Date:** 2026-06-22
|
||||
**Status:** Approved (brainstorming complete)
|
||||
**Scope:** Two new plugins (`market-research`, `hermes-build`) plus a one-branch edit to the
|
||||
existing `clone-app` plugin, chaining into a full *discover → analyze → build* pipeline.
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
Give the user an end-to-end loop:
|
||||
|
||||
1. **Discover** — autonomously research the app/game market and surface ≥10 fresh,
|
||||
scored clone candidates that do not repeat across runs.
|
||||
2. **Pick** — the user selects one or more candidates.
|
||||
3. **Analyze** — each pick flows into the existing `clone-app` feasibility skill.
|
||||
4. **Build** — on approval, hand off to a new `hermes-build` skill that generates the
|
||||
capability bundle [hermes-agent](https://github.com/nousresearch/hermes-agent) lacks
|
||||
(Firebase, Play publish, ads) and best-effort drives hermes to build → test →
|
||||
Firebase → ads → upload to the Play **internal testing** track.
|
||||
|
||||
Effort throughout stays measured in **AI Sprints** (one focused session), never calendar time —
|
||||
consistent with the existing clone-app convention.
|
||||
|
||||
## 2. The hard constraint (unchanged)
|
||||
|
||||
`plugins/android-reverse-engineering/` stays byte-identical to upstream. Before any commit:
|
||||
|
||||
```bash
|
||||
git status --porcelain plugins/android-reverse-engineering/ # must print nothing
|
||||
```
|
||||
|
||||
All new work lives under `plugins/market-research/` and `plugins/hermes-build/`. The only
|
||||
edit to shared/existing files: `clone-app`'s Phase 7 (one new branch) and root
|
||||
`.claude-plugin/marketplace.json` (two new plugin entries).
|
||||
|
||||
## 3. hermes-agent — what it is, what it lacks
|
||||
|
||||
Established by research (github.com/nousresearch/hermes-agent, MIT, v0.17.0):
|
||||
|
||||
- **Is:** a self-hosted agent framework + runtime. Executes shell/Python/build tools/tests/git
|
||||
across six backends (local, Docker, SSH, Singularity, Modal, Daytona). Any LLM provider
|
||||
(no vendor lock-in). Interfaces: CLI (`hermes`), Python SDK (`from run_agent import AIAgent`),
|
||||
HTTP gateway.
|
||||
- **Lacks (no built-in tool):** Firebase integration, Google Play publisher automation, AdMob/ads
|
||||
configuration, Android emulator orchestration. These are added via **custom hermes skills**
|
||||
(`~/.hermes/skills/`), **MCP servers**, or **shell delegation** (`firebase`, `gcloud`,
|
||||
`bundletool`, `fastlane`).
|
||||
|
||||
**Consequence:** "fully autonomous dev→publish" is not a built-in button. `hermes-build` generates
|
||||
the missing capabilities as a bundle, then lets hermes execute them. This is the integration model
|
||||
the user chose (generate skill + MCP config bundle).
|
||||
|
||||
## 4. Architecture
|
||||
|
||||
```
|
||||
plugins/
|
||||
android-reverse-engineering/ [upstream, untouched]
|
||||
clone-app/ [existing — only Phase 7 gains one branch]
|
||||
market-research/ [NEW]
|
||||
hermes-build/ [NEW]
|
||||
```
|
||||
|
||||
**Pipeline (chained handoffs, no central orchestrator):**
|
||||
|
||||
```
|
||||
/market-research
|
||||
→ free web gather + LLM trend synthesis, history-excluded
|
||||
→ ≥10 scored ideas → user picks N
|
||||
→ for each pick: offer "run clone-app feasibility?" → invoke clone-app skill
|
||||
clone-app (existing 8 phases, unchanged)
|
||||
→ Phase 7 decision gate, NEW third branch "Build with hermes"
|
||||
→ invoke hermes-build (passing report + stack + payloads.json)
|
||||
hermes-build
|
||||
→ preflight (best-effort) → mission brief → capability bundle → drive hermes
|
||||
→ signed AAB → Play internal testing track → build-report
|
||||
```
|
||||
|
||||
Handoff style matches the existing `clone-app → writing-plans` chain. Each plugin is independently
|
||||
installable and testable.
|
||||
|
||||
## 5. Decisions locked in brainstorming
|
||||
|
||||
| # | Decision | Choice |
|
||||
|---|----------|--------|
|
||||
| Scope | How much to build | Full pipeline, end to end |
|
||||
| Data source | Market research inputs | Free web (WebSearch + Play charts + App Store RSS) **+** LLM trend synthesis. $0, no paid intel API. |
|
||||
| Freshness | Avoid repeat ideas | History file; each run excludes/deprioritizes prior suggestions |
|
||||
| Ranking | What makes a good candidate | Composite score: **Cloneability + Market opportunity + Monetization fit** (primary); niche gap (secondary tiebreaker) |
|
||||
| Hermes integration | How we drive hermes | Generate hermes **skill + MCP config bundle** that fills hermes's gaps |
|
||||
| Publish boundary | How far autonomy goes | Build signed AAB → **Play internal testing track only**. Stops before production. Reversible. |
|
||||
| Packaging | Repo layout | **Three plugins** (market-research, clone-app, hermes-build) |
|
||||
| Orchestration | Who drives the chain | **Chained handoffs** — each skill offers the next |
|
||||
| Missing prereqs | hermes/creds not set up | **Best-effort + skip** with warnings. Never half-fails. |
|
||||
|
||||
## 6. Plugin: `market-research`
|
||||
|
||||
**Goal:** autonomous market scan → ≥10 scored, non-repeating clone candidates.
|
||||
|
||||
**Skill:** `skills/market-research/SKILL.md` — phased prose orchestrator (clone-app pattern).
|
||||
|
||||
**Phases:**
|
||||
|
||||
- **P0 — Seed rotation.** Pick varied search angles this run (category × region × niche), read
|
||||
from `references/research-angles.md` (rotating list, e.g. hyper-casual games, utilities,
|
||||
fintech LATAM, AI tools, health). Variety comes from rotated seeds + history exclusion.
|
||||
- **P1 — Gather (free web).** WebSearch + scrape: Play top-charts pages, App Store RSS feeds
|
||||
(`itunes.apple.com/<region>/rss/topfreeapplications`), ProductHunt/Reddit/trend articles.
|
||||
Helper `fetch-charts.py` (stdlib, offline-fixture-tested) returns structured chart data; the
|
||||
model synthesizes emerging trends from the search results.
|
||||
- **P2 — Synthesize candidates.** LLM clusters findings into ≥10 distinct ideas; each carries:
|
||||
name, category, what-it-does, why-now (trend signal), incumbent(s), monetization model.
|
||||
- **P3 — Score.** Composite per `references/scoring-guide.md`: cloneability (tech-stack-simplicity
|
||||
guess, backend surface, no heavy ML), market opportunity (demand/growth/weak incumbents),
|
||||
monetization fit (ads/IAP, ARPU category) — all primary; niche gap secondary. Weighted 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-<YYYY-MM-DD>.md` — this run's full report.
|
||||
|
||||
**Scripts** (`scripts/`, each testable, stdlib-only):
|
||||
- `fetch-charts.py` — chart/RSS fetch → JSON; `--html-file` / `--json-file` for offline fixtures.
|
||||
- `history.py` — read / append / dedup `history.json`.
|
||||
|
||||
**References** (`references/`): `research-angles.md`, `scoring-guide.md`, `report-template.md`.
|
||||
|
||||
**Tests** (`tests/`): chart/RSS fixtures, history-dedup test, scoring sanity, `smoke-structure.sh`.
|
||||
Offline-fixture pattern, no network — same discipline as clone-app.
|
||||
|
||||
## 7. Plugin: `clone-app` (edit only)
|
||||
|
||||
Single change: **Phase 7 decision gate** gains a third branch.
|
||||
|
||||
Existing branches: **Yes** → `superpowers:writing-plans`; **No** → stop.
|
||||
New branch: **"Build with hermes"** → invoke `hermes-build`, passing the clone report, the
|
||||
user-selected stack (Phase 4), and `$WORK/payloads.json` (backend API surface).
|
||||
|
||||
No other clone-app file changes. The upstream RE plugin is not touched.
|
||||
|
||||
## 8. Plugin: `hermes-build`
|
||||
|
||||
**Goal:** take a clone-report + plan, generate the capability bundle hermes lacks, then best-effort
|
||||
drive hermes to build → test → Firebase → ads → Play internal-testing track.
|
||||
|
||||
**Skill:** `skills/hermes-build/SKILL.md` — phased.
|
||||
|
||||
**Phases:**
|
||||
|
||||
- **P0 — Preflight (best-effort, never blocks).** Probe for: `hermes` binary, `firebase-tools`,
|
||||
`fastlane`/`bundletool`, Play service-account JSON, signing keystore, AdMob app/unit IDs. Write
|
||||
`preflight.json` (available/missing). Each missing capability marks its later steps `SKIP` with a
|
||||
warning; the pipeline continues.
|
||||
- **P1 — Generate mission brief.** `mission-brief.md` = structured spec hermes consumes: app name,
|
||||
target stack, feature list, backend API surface (from `payloads.json`), Firebase services needed,
|
||||
ads placement, build target = **signed AAB → Play internal testing only** (stops before
|
||||
production).
|
||||
- **P2 — Generate hermes capability bundle.** Emit into `./work/<pkg>/hermes/`:
|
||||
- hermes **skills** (hermes skill format): `firebase-setup`, `play-publish`
|
||||
(fastlane supply → internal track), `admob-wire`, `android-build` (gradle / AAB sign).
|
||||
- **MCP config** stub (Firebase CLI / Play API where MCP fits).
|
||||
- `hermes.config` snippet (backend = Modal/Docker, model provider).
|
||||
- Steps whose prereq was missing in P0 are marked `SKIP` in the brief.
|
||||
- **P3 — Drive hermes (best-effort).** If `hermes` present: feed the mission brief to hermes
|
||||
(`hermes` CLI / SDK `AIAgent.run_conversation`), stream progress — hermes autonomously scaffolds →
|
||||
builds → tests → Firebase init → ads wire → signs AAB → uploads internal track. If `hermes`
|
||||
absent: skip the drive, hand the user the bundle + brief + run instructions.
|
||||
- **P4 — Report.** `build-report-<YYYY-MM-DD>.md`: what ran, what skipped (and why — missing creds),
|
||||
AAB path, internal-track upload status, and explicit next manual steps. Promotion to production is
|
||||
manual and out of scope.
|
||||
|
||||
**State** — `./work/<pkg>/hermes/`: bundle files, `mission-brief.md`, `preflight.json`,
|
||||
`build-report-<date>.md`.
|
||||
|
||||
**Scripts** (`scripts/`, testable): `preflight.sh` (capability probe → JSON), `gen-bundle.py`
|
||||
(templates → hermes skill files; template-driven, fixture-tested), `drive-hermes.sh` (invoke hermes
|
||||
if present, else print instructions).
|
||||
|
||||
**References:** `bundle-templates/` (firebase / play / admob / android-build skill templates),
|
||||
`mission-brief-template.md`.
|
||||
|
||||
**Tests:** preflight-probe (mock present/missing), gen-bundle output vs fixtures,
|
||||
`smoke-structure.sh`. No live hermes / Play / Firebase calls in tests.
|
||||
|
||||
**Boundaries (explicit):**
|
||||
- Never auto-promotes past the internal testing track to production.
|
||||
- Never spends money or publishes without pre-existing user credentials.
|
||||
- Missing creds → skip + warn, never a hard fail.
|
||||
- Legal/publishing liability surfaced in the build-report; the existing clone-app legal note still
|
||||
governs which apps may be analyzed/cloned at all.
|
||||
|
||||
## 9. Cross-cutting conventions (inherited from clone-app)
|
||||
|
||||
- **Working dir** is `./work/...` relative to the user's cwd — never inside any plugin.
|
||||
- **Scripts** use `#!/usr/bin/env bash`, run via `bash <path>`; Python is **stdlib-only**
|
||||
(`urllib`, `json`, `re`); no pip, no virtualenv.
|
||||
- **Tests** use `set -uo pipefail`, aggregate failures, exit non-zero if any fail. Python scrapers
|
||||
tested offline against `tests/fixtures/` via `--html-file` / `--json-file`. New scrape logic needs
|
||||
a fixture, not a live call.
|
||||
- **Commits** follow Conventional Commits scoped per plugin:
|
||||
`feat(market-research): …`, `feat(hermes-build): …`.
|
||||
- **Marketplace:** add both plugins to root `.claude-plugin/marketplace.json`; each plugin carries
|
||||
its own `.claude-plugin/plugin.json`.
|
||||
|
||||
## 10. Build order (each gets its own implementation plan)
|
||||
|
||||
This umbrella spec decomposes into three implementation plans, built and tested in order:
|
||||
|
||||
1. **`market-research`** — standalone value first; hands to existing clone-app manually until the
|
||||
chain edit lands.
|
||||
2. **`clone-app` Phase 7 edit** — one branch; small.
|
||||
3. **`hermes-build`** — largest; depends on the brief/bundle contract.
|
||||
|
||||
Each plan is produced via the `writing-plans` skill and verified by its own test suite before the
|
||||
next begins.
|
||||
|
||||
## 11. Out of scope
|
||||
|
||||
- Paid market-intelligence APIs (SensorTower / data.ai / AppMagic).
|
||||
- Autonomous production publishing, live AdMob spend, or any irreversible Play Console action.
|
||||
- Installing/bootstrapping hermes or its credentials on the user's machine (best-effort detect only).
|
||||
- Modifying the upstream `android-reverse-engineering` plugin.
|
||||
- iOS build/publish (Play/Android only).
|
||||
|
|
@ -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/"
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
# market-research
|
||||
|
||||
A Claude Code plugin that autonomously researches the app/game market and surfaces
|
||||
scored, non-repeating clone candidates, then hands user-chosen candidates to the
|
||||
`clone-app` plugin for feasibility analysis.
|
||||
|
||||
## What it does
|
||||
|
||||
`/market-research` runs a 6-phase workflow:
|
||||
|
||||
0. **Seed rotation** — pick varied search angles (category × region × niche) so runs differ.
|
||||
1. **Gather** — App Store RSS chart data (`fetch-charts.py`) + free web search for trends.
|
||||
2. **Synthesize** — cluster findings into ≥10 distinct app/game ideas.
|
||||
3. **Score** — composite score: cloneability + market opportunity + monetization fit.
|
||||
4. **Dedup** — exclude anything already in `./work/market-research/history.json`.
|
||||
5. **Present + handoff** — show the ranked table; chosen candidates flow into `clone-app`.
|
||||
|
||||
## State
|
||||
|
||||
Written under `./work/market-research/` in your current directory:
|
||||
- `history.json` — every past suggestion; the non-repeat memory.
|
||||
- `research-<date>.md` — the full report for a run.
|
||||
|
||||
## Scripts
|
||||
|
||||
- `scripts/fetch-charts.py` — fetch App Store RSS chart feeds → normalized JSON.
|
||||
- `scripts/history.py` — read / append / dedup the suggestion history.
|
||||
|
||||
Python is stdlib-only. Tests run offline against `tests/fixtures/`:
|
||||
|
||||
```bash
|
||||
bash plugins/market-research/tests/run-all.sh
|
||||
```
|
||||
|
||||
## Effort convention
|
||||
|
||||
Effort is measured in **AI Sprints** (one focused Claude session), never calendar time.
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
allowed-tools: Bash, Read, Glob, Grep, Write, Edit, WebFetch, WebSearch, Skill
|
||||
description: Research the app/game market and surface scored, non-repeating clone candidates
|
||||
user-invocable: true
|
||||
argument-hint: [optional focus, e.g. "casual games" or "fintech LATAM"]
|
||||
argument: optional market focus or angle (optional)
|
||||
---
|
||||
|
||||
# /market-research
|
||||
|
||||
Run the market-research workflow: scan the market, score candidates, hand picks to clone-app.
|
||||
|
||||
## Instructions
|
||||
|
||||
Follow the market-research skill workflow in
|
||||
`${CLAUDE_PLUGIN_ROOT}/skills/market-research/SKILL.md` exactly, phases 0 through 5.
|
||||
|
||||
### Step 1: Optional focus
|
||||
If the user passed a focus argument (e.g. "casual games", "fintech LATAM"), bias
|
||||
the Phase 0 seed selection toward it. Otherwise rotate seeds normally.
|
||||
|
||||
### Step 2: Run the skill
|
||||
Execute Phase 0 → Phase 5 from SKILL.md. Pause for the user at Phase 5 (pick
|
||||
candidates to hand to clone-app).
|
||||
|
||||
### Step 3: Deliver
|
||||
Ensure the report is written to `./work/market-research/research-<date>.md` and
|
||||
the new suggestions are appended to `./work/market-research/history.json`. For
|
||||
each candidate the user picks, resolve it to a Google Play package/URL and invoke
|
||||
the `clone-app` skill on it.
|
||||
|
|
@ -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-<YYYY-MM-DD>.md` — this run's report.
|
||||
|
||||
Create it: `WORK="./work/market-research"` and `mkdir -p "$WORK"`.
|
||||
|
||||
Pick a `RUN_ID` for this run that encodes the chosen angles (e.g.
|
||||
`2026-06-22-games-br`); it is stored with each suggestion so future runs can see
|
||||
which angles were used recently.
|
||||
|
||||
## Phase 0: Seed rotation
|
||||
Read `${CLAUDE_PLUGIN_ROOT}/skills/market-research/references/research-angles.md`.
|
||||
If `$WORK/history.json` exists, skim recent `run_id`s to see which angles were
|
||||
used lately. Choose 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 <region> --limit 25 > "$WORK/charts-<region>-free.json"
|
||||
python3 ${CLAUDE_PLUGIN_ROOT}/skills/market-research/scripts/fetch-charts.py \
|
||||
topgrossingapplications --region <region> --limit 25 > "$WORK/charts-<region>-grossing.json"
|
||||
```
|
||||
(Top-grossing = monetization signal; top-free = demand signal. Add
|
||||
`toppaidapplications` if willingness-to-pay matters for the angle.) If a fetch
|
||||
fails, note it and continue with the feeds you got.
|
||||
|
||||
Trend signal — use WebSearch for the chosen categories/niches: new releases,
|
||||
ProductHunt launches, Reddit/news chatter in the last ~90 days, "fastest growing
|
||||
<category> apps 2026", dated-incumbent complaints. Vary the queries by the
|
||||
run's angles so two runs don't search the same terms. These results are the
|
||||
qualitative half the charts can't give.
|
||||
|
||||
## Phase 2: Synthesize candidates
|
||||
Cluster the chart entries + web findings into **at least 12** distinct app/game
|
||||
ideas (synthesize more than 10 so dedup in Phase 4 still leaves ≥10). For each:
|
||||
name, category, what-it-does, why-now (trend signal), incumbent(s), monetization
|
||||
model. Note that App Store `bundle_id`s from the charts are iOS — treat them as
|
||||
signal, not Android packages. Write the working list as a JSON array (objects
|
||||
with at least `name`, optional `package`, `category`) to `$WORK/candidates.json`.
|
||||
|
||||
## Phase 3: Score
|
||||
Read `${CLAUDE_PLUGIN_ROOT}/skills/market-research/references/scoring-guide.md`.
|
||||
Score every candidate's four components and weighted total. Add the subscores and
|
||||
`total` to each object in `$WORK/candidates.json`. Rank by `total` descending.
|
||||
|
||||
## Phase 4: History dedup
|
||||
Drop anything already suggested:
|
||||
```bash
|
||||
python3 ${CLAUDE_PLUGIN_ROOT}/skills/market-research/scripts/history.py \
|
||||
filter --history "$WORK/history.json" < "$WORK/candidates.json" > "$WORK/fresh.json"
|
||||
```
|
||||
If fewer than 10 candidates survive, go back to Phase 1/2 with a different angle
|
||||
and synthesize more, then re-filter — never present a padded or repeated list.
|
||||
|
||||
## Phase 5: Present + handoff
|
||||
Read `${CLAUDE_PLUGIN_ROOT}/skills/market-research/references/report-template.md`.
|
||||
Fill it from `$WORK/fresh.json` and write `$WORK/research-<YYYY-MM-DD>.md` (use
|
||||
the actual run date). Show the user the ranked table (≥10 rows) and your top-3
|
||||
recommended picks.
|
||||
|
||||
Record this run's suggestions so they won't repeat:
|
||||
```bash
|
||||
python3 ${CLAUDE_PLUGIN_ROOT}/skills/market-research/scripts/history.py \
|
||||
add --history "$WORK/history.json" --date <YYYY-MM-DD> --run-id "<RUN_ID>" \
|
||||
< "$WORK/fresh.json"
|
||||
```
|
||||
|
||||
Then ask which candidate(s) to pursue. For each pick:
|
||||
1. Resolve it to a Google Play package/URL. If you don't already have the package,
|
||||
WebSearch `"<name>" site:play.google.com` (or the developer + app name) and
|
||||
confirm the `play.google.com/store/apps/details?id=...` URL.
|
||||
2. Invoke the `clone-app` skill on that URL/package to run full feasibility.
|
||||
If the user picks nothing, stop — the report stands on its own.
|
||||
|
||||
## Error Handling Summary
|
||||
| Scenario | Action |
|
||||
|---|---|
|
||||
| `fetch-charts.py` fails for a region | note it, continue with other feeds/web search |
|
||||
| Web search returns thin results | broaden queries within the chosen angle, try another region |
|
||||
| < 10 candidates survive dedup | loop back to Phase 1/2 with a new angle, re-filter |
|
||||
| history.json missing/first run | treat as empty; all candidates are fresh |
|
||||
| Candidate has no resolvable Play package | skip the handoff for it, keep it in the report as iOS-only/unresolved |
|
||||
| User picks nothing | stop after writing the report |
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
# Market Research Report Template
|
||||
|
||||
Fill every section. Write to `./work/market-research/research-<YYYY-MM-DD>.md`.
|
||||
|
||||
---
|
||||
|
||||
# Market Research — <YYYY-MM-DD>
|
||||
|
||||
## Run parameters
|
||||
- Angles this run: <categories / regions / niche lens chosen in Phase 0>
|
||||
- Focus argument: <the user's focus, or "none">
|
||||
- Sources: App Store RSS (<feeds/regions pulled>) + web search (<themes>)
|
||||
- Candidates after history exclusion: <N> (history had <M> prior suggestions)
|
||||
|
||||
## Top candidates (ranked)
|
||||
|
||||
| # | Name | Category | Clone | Market | Monet. | Niche | **Total** | Why now |
|
||||
|---|------|----------|------:|-------:|-------:|------:|----------:|---------|
|
||||
| 1 | … | … | 85 | 80 | 75 | 60 | **79** | one-line trend rationale |
|
||||
| … | | | | | | | | |
|
||||
|
||||
(At least 10 rows.)
|
||||
|
||||
## Candidate detail
|
||||
|
||||
For each of the top candidates:
|
||||
|
||||
### <name>
|
||||
- **Package (if known):** <com.x.y or "to resolve">
|
||||
- **What it does:** <1–2 sentences>
|
||||
- **Why now:** <trend signal: chart movement, news, dated incumbent…>
|
||||
- **Incumbents:** <who already does this and how weak/strong>
|
||||
- **Monetization:** <ads / IAP / subscription; ARPU note>
|
||||
- **Scores:** clone <>, market <>, monetization <>, niche <> → **total <>**
|
||||
- **Clone risk flags:** <heavy ML / native / content moat / none>
|
||||
|
||||
## Recommended picks
|
||||
|
||||
Top 3 to send to clone-app first, with one sentence each on why they lead.
|
||||
|
||||
## Next step
|
||||
|
||||
The user picks one or more candidates; each chosen candidate is resolved to a
|
||||
Google Play package/URL and handed to the `clone-app` skill for full feasibility.
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Fetch an Apple App Store RSS chart feed into a normalized JSON list.
|
||||
|
||||
Apple publishes public, no-auth RSS chart feeds as JSON at
|
||||
https://itunes.apple.com/<region>/rss/<feed>/limit=<n>/json . They give a
|
||||
ranked list of trending apps (name, developer, category, iOS bundle id, price).
|
||||
|
||||
This is iOS chart data — bundle ids are iOS bundle ids, NOT Android packages.
|
||||
The market-research skill uses these as trend signal and resolves a Google Play
|
||||
package later (Phase 5) before any clone-app handoff. Stdlib-only, no pip.
|
||||
"""
|
||||
import sys, json, argparse, urllib.request, ssl, subprocess, shutil
|
||||
|
||||
FEEDS = ("topfreeapplications", "toppaidapplications", "topgrossingapplications")
|
||||
UA = "Mozilla/5.0"
|
||||
|
||||
def _http_get(url):
|
||||
"""GET a URL as text. urllib first; on an SSL trust failure (macOS system
|
||||
Python ships without a CA bundle) fall back to system `curl`."""
|
||||
req = urllib.request.Request(url, headers={"User-Agent": UA})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
return r.read().decode("utf-8", "replace")
|
||||
except urllib.error.URLError as e:
|
||||
if not isinstance(e.reason, ssl.SSLError):
|
||||
raise
|
||||
if not shutil.which("curl"):
|
||||
raise
|
||||
out = subprocess.run(["curl", "-sL", "--fail", "-A", UA, url],
|
||||
capture_output=True, timeout=60)
|
||||
if out.returncode != 0:
|
||||
raise
|
||||
return out.stdout.decode("utf-8", "replace")
|
||||
|
||||
def fetch(feed, region, limit):
|
||||
url = f"https://itunes.apple.com/{region}/rss/{feed}/limit={limit}/json"
|
||||
return json.loads(_http_get(url))
|
||||
|
||||
def _label(node):
|
||||
"""Apple wraps text values as {"label": "..."}; return the label or None."""
|
||||
if isinstance(node, dict):
|
||||
return node.get("label")
|
||||
return None
|
||||
|
||||
def normalize(data):
|
||||
entries = []
|
||||
feed = data.get("feed") or {}
|
||||
raw = feed.get("entry") or []
|
||||
if isinstance(raw, dict): # Apple collapses a single entry to a dict
|
||||
raw = [raw]
|
||||
for i, e in enumerate(raw, start=1):
|
||||
cat = (e.get("category") or {}).get("attributes") or {}
|
||||
idattr = (e.get("id") or {}).get("attributes") or {}
|
||||
price = (e.get("im:price") or {}).get("attributes") or {}
|
||||
entries.append({
|
||||
"rank": i,
|
||||
"name": _label(e.get("im:name")),
|
||||
"developer": _label(e.get("im:artist")),
|
||||
"category": cat.get("label"),
|
||||
"bundle_id": idattr.get("im:bundleId"),
|
||||
"price": price.get("amount"),
|
||||
})
|
||||
return entries
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("feed", choices=FEEDS)
|
||||
ap.add_argument("--region", default="us")
|
||||
ap.add_argument("--limit", type=int, default=25)
|
||||
ap.add_argument("--json-file")
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.json_file:
|
||||
with open(args.json_file, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
else:
|
||||
try:
|
||||
data = fetch(args.feed, args.region, args.limit)
|
||||
except Exception as e:
|
||||
print(f"ERROR: failed to fetch RSS feed: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
entries = normalize(data)
|
||||
print(json.dumps({
|
||||
"feed": args.feed,
|
||||
"region": args.region,
|
||||
"count": len(entries),
|
||||
"entries": entries,
|
||||
}, indent=2))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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" }
|
||||
]
|
||||
|
|
@ -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" }
|
||||
]
|
||||
}
|
||||
|
|
@ -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" } }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue