@opendirectory.dev/skills 0.1.34 → 0.1.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opendirectory.dev/skills",
3
- "version": "0.1.34",
3
+ "version": "0.1.35",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "bin": {
package/registry.json CHANGED
@@ -77,6 +77,16 @@
77
77
  "version": "1.0.0",
78
78
  "path": "skills/explain-this-pr"
79
79
  },
80
+ {
81
+ "name": "gh-issue-to-demand-signal",
82
+ "description": "Takes a competitor's public GitHub repo URL, fetches their open issues via the GitHub REST API, filters noise locally, clusters issues into 6 deman...",
83
+ "tags": [
84
+ "Developer Tools"
85
+ ],
86
+ "author": "opendirectory",
87
+ "version": "0.0.1",
88
+ "path": "skills/gh-issue-to-demand-signal"
89
+ },
80
90
  {
81
91
  "name": "google-trends-api-skills",
82
92
  "description": "SEO keyword research workflow for blog generation using Google Trends data.",
@@ -0,0 +1,3 @@
1
+ GITHUB_TOKEN= # optional -- github.com/settings/tokens (no scopes needed for public repos)
2
+ # Without it: 60 req/hr unauthenticated (enough for ~2 fetches before hitting limit)
3
+ # With it: 5000 req/hr (effectively unlimited for this skill's 2-page fetch pattern)
@@ -0,0 +1,118 @@
1
+ # gh-issue-to-demand-signal
2
+
3
+ Give the skill a competitor's public GitHub repo URL. It fetches their open issues, filters noise locally, clusters into 6 demand categories using the AI already running the skill, scores by real engagement (reactions), detects ignored demand (high reactions + no response = your opportunity), and outputs a ranked demand gap report with a GTM messaging brief.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npx "@opendirectory.dev/skills" install gh-issue-to-demand-signal --target claude
9
+ ```
10
+
11
+ ### Video Tutorial
12
+ Watch this quick video to see how it's done:
13
+
14
+ https://github.com/user-attachments/assets/ee98a1b5-ebc4-452f-bbfb-c434f2935067
15
+
16
+ ### Step 1: Download the skill from GitHub
17
+ 1. Click the **Code** button on this repo's GitHub page.
18
+ 2. Select **Download ZIP** to download the repository.
19
+ 3. Extract the ZIP file on your computer.
20
+
21
+ ### Step 2: Install the Skill in Claude
22
+ 1. Open your **Claude desktop app**.
23
+ 2. Go to the sidebar on the left side and click on the **Customize** section.
24
+ 3. Click on the **Skills** tab, then click on the **+** (plus) icon button to create a new skill.
25
+ 4. Choose the option to **Upload a skill**, and drag and drop the `.zip` file (or you can extract it and drop the folder, both work).
26
+
27
+ > **Note:** For some skills (like `position-me`), the `SKILL.md` file might be located inside a subfolder. Always make sure you are uploading the specific folder that contains the `SKILL.md` file!
28
+
29
+ ## What It Does
30
+
31
+ - Fetches up to 200 open issues from any public GitHub repo (no auth required)
32
+ - Filters noise locally: removes PRs, bot issues, zero-engagement issues, and chore-pattern titles
33
+ - Computes a demand score per issue: `(reactions["+1"] x 2) + (comments x 0.5)`
34
+ - Detects "ignored demand": issues with 10+ reactions, open 6+ months, no planned label
35
+ - Clusters issues into 6 categories using the AI running the skill (no external API key needed): feature gaps, bug patterns, UX complaints, performance issues, missing integrations, missing docs
36
+ - Generates 5-8 cluster themes with total demand scores
37
+ - Messaging brief: 3 positioning angles + 3 outreach hooks + cluster-specific headlines
38
+ - Saves output to `docs/demand-signals/[owner]-[repo]-[date].md`
39
+
40
+ ## Requirements
41
+
42
+ | Requirement | Purpose | How to Set Up |
43
+ |---|---|---|
44
+ | GitHub token | Raises rate limit from 60/hr to 5000/hr | github.com/settings/tokens (no scopes needed for public repos) |
45
+
46
+ No external AI API key needed. The skill uses the AI assistant running it (Claude, Gemini, Copilot) to do the clustering and messaging brief. GitHub token is optional but recommended for repeated use.
47
+
48
+ ## Setup
49
+
50
+ ```bash
51
+ cp .env.example .env
52
+ # Add GITHUB_TOKEN (optional, recommended for repeated use)
53
+ ```
54
+
55
+ ## How to Use
56
+
57
+ ```
58
+ "Scan competitor GitHub issues: https://github.com/vercel/next.js"
59
+ "What are users asking for in facebook/react?"
60
+ "Find demand gaps in linear-app/linear"
61
+ "Turn GitHub complaints into messaging: https://github.com/supabase/supabase"
62
+ "What should I build based on competitor issues? vercel/next.js"
63
+ "Which features are users begging for in this repo?"
64
+ ```
65
+
66
+ ## Why This Matters
67
+
68
+ A busy product team cannot read 500 open issues. This skill does the work: ranks by real user demand (reaction counts), clusters into actionable GTM categories, detects ignored demand (where competitors have been silent the longest), and translates raw complaints into positioning language.
69
+
70
+ **The demand score formula:** `(reactions["+1"] x 2) + (comments x 0.5)`. Reactions are weighted higher than comments because reactions are pure signal -- a thumbs-up means "I want this." Comments include maintainer responses, off-topic discussion, and spam.
71
+
72
+ **Ignored demand is your opportunity:** An issue with 50 reactions that has been open for 2 years with no planned label means their users have been waiting. That is your product's first billboard.
73
+
74
+ ## The 6 Demand Categories
75
+
76
+ | Category | What it captures |
77
+ |---|---|
78
+ | `feature_gap` | Functionality that does not exist yet |
79
+ | `bug_pattern` | Recurring broken behavior that erodes trust |
80
+ | `ux_complaint` | Friction, confusion, or workflow problems |
81
+ | `performance` | Slowness, timeouts, resource usage |
82
+ | `integration_missing` | Requests to connect with other tools or APIs |
83
+ | `docs_missing` | Confusion caused by absent or wrong documentation |
84
+
85
+ ## Output
86
+
87
+ Each run produces:
88
+
89
+ 1. **Demand Gap Leaderboard**: clusters ranked by total demand score
90
+ 2. **Ignored Demand section**: issues open 6+ months with 10+ reactions and no response
91
+ 3. **Top 10 Highest-Demand Issues**: verbatim titles, reaction counts, direct GitHub links
92
+ 4. **Cluster Deep Dives**: top 3 clusters with theme name, pain summary, top 3 verbatim issues
93
+ 5. **Messaging Brief**: 3 positioning angles citing exact demand evidence
94
+ 6. **GTM Angles**: 3 outreach hooks using verbatim issue language
95
+
96
+ ## Cost per Run
97
+
98
+ - GitHub API: 2 calls -- free (unauthenticated: 60/hr limit, token: 5000/hr)
99
+ - AI analysis: uses the model already running the skill -- no additional cost
100
+ - Total: free
101
+
102
+ ## Project Structure
103
+
104
+ ```
105
+ gh-issue-to-demand-signal/
106
+ ├── SKILL.md
107
+ ├── README.md
108
+ ├── .env.example
109
+ ├── evals/
110
+ │ └── evals.json
111
+ └── references/
112
+ ├── demand-categories.md
113
+ └── gtm-translation.md
114
+ ```
115
+
116
+ ## License
117
+
118
+ MIT
@@ -0,0 +1,638 @@
1
+ ---
2
+ name: gh-issue-to-demand-signal
3
+ description: Takes a competitor's public GitHub repo URL, fetches their open issues via the GitHub REST API, filters noise locally, clusters issues into 6 demand categories, computes a demand score per issue and per cluster, and outputs a ranked demand gap report with a GTM messaging brief. Use when asked to scan a competitor's GitHub issues, find what their users are begging for, turn GitHub complaints into product positioning, identify competitor feature gaps, or generate messaging from real user demand. Trigger when a user says "scan competitor issues", "what are users asking for on X repo", "find demand gaps in Y", "turn GitHub issues into messaging", or "what should I build based on competitor complaints".
4
+ compatibility: [claude-code, gemini-cli, github-copilot]
5
+ ---
6
+
7
+ # GitHub Issue Demand Signal
8
+
9
+ Take a competitor's public GitHub repo. Fetch their open issues. Filter noise locally. Cluster into 6 demand categories. Score by real engagement. Output a ranked demand gap report and GTM messaging brief.
10
+
11
+ ---
12
+
13
+ **Critical rule:** Every issue title in the output must be verbatim from the GitHub API response. Every cluster theme name must be derived from actual issue titles in that cluster. If fewer than 10 issues remain after noise filtering, stop and tell the user -- the repo is too small for reliable clustering. No invented issue content anywhere.
14
+
15
+ ---
16
+
17
+ ## Common Mistakes
18
+
19
+ | The agent will want to... | Why that's wrong |
20
+ |---|---|
21
+ | Send all 200 raw issues to the AI without filtering | Bot issues, PRs, and zero-engagement noise inflate cluster counts and waste context. Filter locally first. |
22
+ | Use comment count as the primary demand signal | Comments include maintainer responses, off-topic discussion, and spam. reactions["+1"] is the cleanest buyer signal. |
23
+ | Paraphrase issue titles when summarizing clusters | Paraphrasing loses the buyer's exact language, which is the entire point. Use verbatim issue titles. |
24
+ | Continue past Step 4 if fewer than 10 issues remain after filtering | Under 10 issues means the repo is too small or the wrong URL was given. Clustering on sparse data produces meaningless categories. |
25
+ | Include pull requests in the analysis | The GitHub Issues endpoint returns PRs too. Filter by checking that the pull_request key is absent on the issue object. |
26
+ | Mark an issue as ignored demand without checking all 3 criteria | All three must be true: reactions >= 10, age >= 180 days, no planned/in-progress/roadmap label. Missing one criterion disqualifies the issue. |
27
+
28
+ ---
29
+
30
+ ## Step 1: Setup Check
31
+
32
+ ```bash
33
+ echo "GITHUB_TOKEN: ${GITHUB_TOKEN:-not set, unauthenticated rate limit applies (60 req/hr)}"
34
+ ```
35
+
36
+ **If GITHUB_TOKEN is not set:** Continue. Tell the user: "GITHUB_TOKEN is not set. Unauthenticated rate limit is 60 requests/hour -- enough for 2 fetches before hitting the limit. For repeated use, add a token at github.com/settings/tokens (no scopes needed for public repos)."
37
+
38
+ ---
39
+
40
+ ## Step 2: Gather Input
41
+
42
+ You need:
43
+ - GitHub repo URL (e.g. https://github.com/owner/repo) or owner/repo slug (e.g. facebook/react)
44
+
45
+ Parse owner and repo from input:
46
+
47
+ ```bash
48
+ python3 << 'PYEOF'
49
+ import re, sys, os
50
+
51
+ raw = "REPO_INPUT_HERE"
52
+
53
+ # Normalize to owner/repo
54
+ if raw.startswith("http"):
55
+ m = re.search(r"github\.com/([^/]+)/([^/?\s]+)", raw)
56
+ if not m:
57
+ print("ERROR: Could not parse GitHub URL. Expected format: https://github.com/owner/repo")
58
+ sys.exit(1)
59
+ owner, repo = m.group(1), m.group(2).rstrip("/")
60
+ elif "/" in raw:
61
+ parts = raw.strip().split("/")
62
+ owner, repo = parts[0], parts[1]
63
+ else:
64
+ print("ERROR: Input must be a GitHub URL or owner/repo slug (e.g. vercel/next.js)")
65
+ sys.exit(1)
66
+
67
+ print(f"Owner: {owner}")
68
+ print(f"Repo: {repo}")
69
+
70
+ with open("/tmp/ghd-target.txt", "w") as f:
71
+ f.write(f"{owner}/{repo}")
72
+ PYEOF
73
+ ```
74
+
75
+ **If parsing fails:** Stop. Ask: "Please provide the GitHub repo as a URL (https://github.com/owner/repo) or an owner/repo slug (e.g. vercel/next.js)."
76
+
77
+ ---
78
+
79
+ ## Step 3: Fetch Issues from GitHub REST API
80
+
81
+ Fetch up to 200 issues (2 pages of 100). Check rate limit after the first fetch.
82
+
83
+ ```bash
84
+ python3 << 'PYEOF'
85
+ import json, urllib.request, os, sys
86
+ from datetime import datetime, timezone
87
+
88
+ target = open("/tmp/ghd-target.txt").read().strip()
89
+ owner_repo = target
90
+ token = os.environ.get("GITHUB_TOKEN", "")
91
+
92
+ headers = {"Accept": "application/vnd.github+json", "User-Agent": "gh-issue-demand-signal/1.0"}
93
+ if token:
94
+ headers["Authorization"] = f"Bearer {token}"
95
+
96
+ all_issues = []
97
+ rate_limit_hit = False
98
+
99
+ for page in [1, 2]:
100
+ url = f"https://api.github.com/repos/{owner_repo}/issues?state=open&per_page=100&page={page}"
101
+ req = urllib.request.Request(url, headers=headers)
102
+
103
+ try:
104
+ with urllib.request.urlopen(req, timeout=30) as resp:
105
+ # Check rate limit after first page
106
+ if page == 1:
107
+ remaining = int(resp.headers.get("X-RateLimit-Remaining", 999))
108
+ reset_ts = resp.headers.get("X-RateLimit-Reset", "")
109
+ if remaining == 0:
110
+ reset_str = datetime.fromtimestamp(int(reset_ts), tz=timezone.utc).strftime("%H:%M UTC") if reset_ts else "unknown"
111
+ print(f"ERROR: GitHub rate limit exhausted. Resets at {reset_str}.")
112
+ print("Add GITHUB_TOKEN to your .env file to get 5000 req/hr. See github.com/settings/tokens (no scopes needed).")
113
+ sys.exit(1)
114
+ print(f"Rate limit remaining: {remaining}")
115
+
116
+ # Check for 404/403
117
+ status = resp.status
118
+ if status == 404:
119
+ print(f"ERROR: Repo '{owner_repo}' not found. Check the URL or slug.")
120
+ sys.exit(1)
121
+
122
+ page_data = json.loads(resp.read())
123
+ if not page_data:
124
+ print(f"Page {page}: empty, stopping.")
125
+ break
126
+ all_issues.extend(page_data)
127
+ print(f"Page {page}: {len(page_data)} issues fetched")
128
+ except urllib.error.HTTPError as e:
129
+ if e.code == 404:
130
+ print(f"ERROR: Repo '{owner_repo}' not found (404). Check the URL or slug.")
131
+ elif e.code == 403:
132
+ print(f"ERROR: Access denied (403). Repo may be private or rate limit hit.")
133
+ else:
134
+ print(f"ERROR: GitHub API returned HTTP {e.code}")
135
+ sys.exit(1)
136
+ except Exception as e:
137
+ print(f"ERROR: Failed to fetch page {page}: {e}")
138
+ sys.exit(1)
139
+
140
+ print(f"Total raw issues fetched: {len(all_issues)}")
141
+ json.dump(all_issues, open("/tmp/ghd-raw-issues.json", "w"), indent=2)
142
+ PYEOF
143
+ ```
144
+
145
+ **If GitHub returns 404:** Stop. Tell the user: "Repo not found. Check the URL or slug and try again. Private repos are not accessible without authentication and explicit repo scope."
146
+
147
+ **If GitHub returns 403 with rate limit header:** Stop. Show the reset time and tell the user to add GITHUB_TOKEN.
148
+
149
+ ---
150
+
151
+ ## Step 4: Pre-Process Locally -- Filter, Score, Detect Ignored Demand
152
+
153
+ No API call. Pure Python. Run before anything goes to the AI.
154
+
155
+ ```bash
156
+ python3 << 'PYEOF'
157
+ import json, re
158
+ from datetime import datetime, timezone
159
+
160
+ raw = json.load(open("/tmp/ghd-raw-issues.json"))
161
+ target = open("/tmp/ghd-target.txt").read().strip()
162
+ now = datetime.now(tz=timezone.utc)
163
+
164
+ noise_patterns = re.compile(
165
+ r"^(chore|deps|bump|renovate|dependabot|release|ci|build|revert)[\s:\[]",
166
+ re.IGNORECASE
167
+ )
168
+ pr_title_patterns = re.compile(
169
+ r"^(feat|fix|refactor|docs|test|style|perf|chore)(\(.+\))?:",
170
+ re.IGNORECASE
171
+ )
172
+
173
+ filtered = []
174
+ noise_count = 0
175
+ noise_reasons = {}
176
+
177
+ for issue in raw:
178
+ # Skip pull requests (GitHub Issues endpoint returns PRs too)
179
+ if "pull_request" in issue:
180
+ noise_count += 1
181
+ noise_reasons["pull_request"] = noise_reasons.get("pull_request", 0) + 1
182
+ continue
183
+
184
+ title = issue.get("title", "")
185
+ reactions = issue.get("reactions", {}).get("+1", 0)
186
+ comments = issue.get("comments", 0)
187
+ user_type = (issue.get("user") or {}).get("type", "User")
188
+
189
+ # Skip bot-authored issues
190
+ if user_type == "Bot":
191
+ noise_count += 1
192
+ noise_reasons["bot_author"] = noise_reasons.get("bot_author", 0) + 1
193
+ continue
194
+
195
+ # Skip bot-pattern titles
196
+ if noise_patterns.match(title):
197
+ noise_count += 1
198
+ noise_reasons["bot_title"] = noise_reasons.get("bot_title", 0) + 1
199
+ continue
200
+
201
+ # Skip PR-as-issue titles
202
+ if pr_title_patterns.match(title):
203
+ noise_count += 1
204
+ noise_reasons["pr_as_issue"] = noise_reasons.get("pr_as_issue", 0) + 1
205
+ continue
206
+
207
+ # Skip zero-signal issues
208
+ if reactions == 0 and comments == 0:
209
+ noise_count += 1
210
+ noise_reasons["zero_signal"] = noise_reasons.get("zero_signal", 0) + 1
211
+ continue
212
+
213
+ # Compute demand score
214
+ demand_score = (reactions * 2) + (comments * 0.5)
215
+
216
+ # Detect ignored demand
217
+ created_at = issue.get("created_at", "")
218
+ if created_at:
219
+ created = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
220
+ age_days = (now - created).days
221
+ else:
222
+ age_days = 0
223
+
224
+ labels = [l.get("name", "").lower() for l in issue.get("labels", [])]
225
+ has_planned_label = any(
226
+ kw in label for label in labels
227
+ for kw in ["in-progress", "planned", "roadmap", "wip", "in progress"]
228
+ )
229
+
230
+ ignored_demand = (
231
+ reactions >= 10 and
232
+ age_days >= 180 and
233
+ not has_planned_label
234
+ )
235
+
236
+ filtered.append({
237
+ "number": issue["number"],
238
+ "title": title,
239
+ "url": issue.get("html_url", f"https://github.com/{target}/issues/{issue['number']}"),
240
+ "reactions_plus1": reactions,
241
+ "comments": comments,
242
+ "demand_score": demand_score,
243
+ "age_days": age_days,
244
+ "labels": labels,
245
+ "ignored_demand": ignored_demand,
246
+ "body_snippet": (issue.get("body") or "")[:300]
247
+ })
248
+
249
+ # Sort by demand score descending
250
+ filtered.sort(key=lambda x: x["demand_score"], reverse=True)
251
+
252
+ print(f"Raw issues: {len(raw)}")
253
+ print(f"Noise filtered: {noise_count} ({', '.join(f'{k}: {v}' for k, v in noise_reasons.items())})")
254
+ print(f"Issues for analysis: {len(filtered)}")
255
+
256
+ if len(filtered) < 10:
257
+ print(f"ERROR: Only {len(filtered)} issues remain after filtering. This repo has too few engaged issues for reliable clustering.")
258
+ print("Try a larger repo or a repo with more community engagement.")
259
+ import sys; sys.exit(1)
260
+
261
+ # Ignored demand summary
262
+ ignored = [i for i in filtered if i["ignored_demand"]]
263
+ print(f"Ignored demand issues: {len(ignored)}")
264
+
265
+ json.dump(filtered, open("/tmp/ghd-filtered-issues.json", "w"), indent=2)
266
+ print("Pre-processing complete.")
267
+ PYEOF
268
+ ```
269
+
270
+ **If fewer than 10 issues remain after filtering:** Stop. Tell the user exactly how many issues were found and filtered, and why the repo is too small for reliable demand clustering.
271
+
272
+ ---
273
+
274
+ ## Step 5: Cluster Issues
275
+
276
+ Print the filtered issues for analysis:
277
+
278
+ ```bash
279
+ python3 << 'PYEOF'
280
+ import json
281
+ filtered = json.load(open("/tmp/ghd-filtered-issues.json"))
282
+ target = open("/tmp/ghd-target.txt").read().strip()
283
+
284
+ issue_list = filtered[:150]
285
+ print(f"Repo: {target}")
286
+ print(f"Issues to cluster: {len(issue_list)}")
287
+ print()
288
+ for i in issue_list:
289
+ labels_str = f" [{', '.join(i['labels'][:3])}]" if i['labels'] else ""
290
+ print(f"#{i['number']} [score:{round(i['demand_score'],1)} reactions:{i['reactions_plus1']}] {i['title']}{labels_str}")
291
+ PYEOF
292
+ ```
293
+
294
+ Classify each issue printed above into one of these 6 categories:
295
+ `feature_gap`, `bug_pattern`, `ux_complaint`, `performance`, `integration_missing`, `docs_missing`
296
+
297
+ Rules:
298
+ - Classify each issue into exactly one category
299
+ - Extract a 1-sentence pain statement using the user's exact language from the title -- do not paraphrase
300
+ - Identify 5-8 cluster themes: short phrases (3-6 words) capturing dominant complaint patterns across all issues
301
+ - No em dashes. No marketing language.
302
+
303
+ Write your analysis to `/tmp/ghd-clusters.json` with this exact structure:
304
+
305
+ ```json
306
+ {
307
+ "classified_issues": [
308
+ {"number": 123, "category": "feature_gap", "pain_statement": "Users need X which does not exist yet"}
309
+ ],
310
+ "cluster_themes": [
311
+ {"theme_name": "Missing export options", "category": "feature_gap", "issue_numbers": [123, 456, 789]}
312
+ ],
313
+ "category_counts": {"feature_gap": 5, "bug_pattern": 3, "ux_complaint": 4, "performance": 2, "integration_missing": 6, "docs_missing": 1}
314
+ }
315
+ ```
316
+
317
+ After writing the file, confirm with:
318
+
319
+ ```bash
320
+ python3 -c "
321
+ import json
322
+ d = json.load(open('/tmp/ghd-clusters.json'))
323
+ print(f'Classified: {len(d[\"classified_issues\"])} issues, {len(d[\"cluster_themes\"])} themes')
324
+ print('Categories:', d['category_counts'])
325
+ "
326
+ ```
327
+
328
+ ---
329
+
330
+ ## Step 6: Messaging Brief
331
+
332
+ Compute total demand score per cluster and print the top 3:
333
+
334
+ ```bash
335
+ python3 << 'PYEOF'
336
+ import json
337
+
338
+ filtered = json.load(open("/tmp/ghd-filtered-issues.json"))
339
+ clusters = json.load(open("/tmp/ghd-clusters.json"))
340
+ target = open("/tmp/ghd-target.txt").read().strip()
341
+
342
+ demand_by_issue = {i["number"]: i["demand_score"] for i in filtered}
343
+ issue_titles = {i["number"]: i["title"] for i in filtered}
344
+ issue_reactions = {i["number"]: i["reactions_plus1"] for i in filtered}
345
+
346
+ enriched_themes = []
347
+ for theme in clusters.get("cluster_themes", []):
348
+ issue_nums = theme.get("issue_numbers", [])
349
+ total_demand = sum(demand_by_issue.get(n, 0) for n in issue_nums)
350
+ top_issues = sorted(issue_nums, key=lambda n: demand_by_issue.get(n, 0), reverse=True)[:3]
351
+ enriched_themes.append({
352
+ "theme_name": theme["theme_name"],
353
+ "category": theme["category"],
354
+ "issue_count": len(issue_nums),
355
+ "total_demand_score": round(total_demand, 1),
356
+ "top_issues": [
357
+ {"number": n, "title": issue_titles.get(n, ""), "reactions": issue_reactions.get(n, 0)}
358
+ for n in top_issues
359
+ ]
360
+ })
361
+
362
+ enriched_themes.sort(key=lambda x: x["total_demand_score"], reverse=True)
363
+ json.dump(enriched_themes, open("/tmp/ghd-enriched-themes.json", "w"), indent=2)
364
+
365
+ print(f"Top 3 clusters for messaging brief (repo: {target}):")
366
+ for t in enriched_themes[:3]:
367
+ print(f"\n {t['theme_name']} ({t['category']}) -- total demand: {t['total_demand_score']}")
368
+ for ti in t["top_issues"]:
369
+ print(f" #{ti['number']}: \"{ti['title']}\" ({ti['reactions']} reactions)")
370
+ PYEOF
371
+ ```
372
+
373
+ Generate a GTM messaging brief from the top 3 clusters printed above.
374
+
375
+ Rules:
376
+ - Each positioning angle must cite the specific cluster it comes from
377
+ - Each outreach hook must quote a verbatim issue title in quotation marks
378
+ - Headlines must include a number or specific named pain -- no generic statements
379
+ - No em dashes. No forbidden words: powerful, robust, seamless, innovative, game-changing, streamline, leverage, transform
380
+
381
+ Write your brief to `/tmp/ghd-brief.json` with this exact structure:
382
+
383
+ ```json
384
+ {
385
+ "positioning_angles": [
386
+ {
387
+ "angle_name": "3-5 word label",
388
+ "cluster_source": "theme_name from cluster",
389
+ "positioning_statement": "2-3 sentences on what your product does that this competitor does not",
390
+ "evidence": "verbatim issue title that best illustrates this gap"
391
+ }
392
+ ],
393
+ "outreach_hooks": [
394
+ {
395
+ "hook_type": "pain quote hook",
396
+ "hook_text": "2-3 sentences quoting a verbatim issue title in quotes",
397
+ "best_for": "audience this hook works for"
398
+ }
399
+ ],
400
+ "cluster_headlines": [
401
+ {
402
+ "theme_name": "from the cluster",
403
+ "headline": "specific headline with a number or named pain",
404
+ "sub_copy": "1 sentence expanding the headline"
405
+ }
406
+ ]
407
+ }
408
+ ```
409
+
410
+ After writing the file, confirm with:
411
+
412
+ ```bash
413
+ python3 -c "
414
+ import json
415
+ d = json.load(open('/tmp/ghd-brief.json'))
416
+ print('Positioning angles:', len(d.get('positioning_angles', [])))
417
+ print('Outreach hooks:', len(d.get('outreach_hooks', [])))
418
+ print('Cluster headlines:', len(d.get('cluster_headlines', [])))
419
+ "
420
+ ```
421
+
422
+ ---
423
+
424
+ ## Step 7: Self-QA
425
+
426
+ Run before presenting. Verify evidence. Remove violations. Check output integrity.
427
+
428
+ ```bash
429
+ python3 << 'PYEOF'
430
+ import json
431
+
432
+ filtered = json.load(open("/tmp/ghd-filtered-issues.json"))
433
+ clusters = json.load(open("/tmp/ghd-clusters.json"))
434
+ themes = json.load(open("/tmp/ghd-enriched-themes.json"))
435
+ brief = json.load(open("/tmp/ghd-brief.json"))
436
+ target = open("/tmp/ghd-target.txt").read().strip()
437
+
438
+ failures = []
439
+ real_titles = {i["number"]: i["title"] for i in filtered}
440
+
441
+ # Verify: classified_issues only reference real issue numbers
442
+ real_numbers = set(real_titles.keys())
443
+ hallucinated = [
444
+ c["number"] for c in clusters.get("classified_issues", [])
445
+ if c["number"] not in real_numbers
446
+ ]
447
+ if hallucinated:
448
+ failures.append(f"Removed {len(hallucinated)} hallucinated issue numbers from classified_issues: {hallucinated[:5]}")
449
+ clusters["classified_issues"] = [
450
+ c for c in clusters.get("classified_issues", [])
451
+ if c["number"] in real_numbers
452
+ ]
453
+
454
+ # Verify: top-10 list is sorted by demand_score descending
455
+ top10 = filtered[:10]
456
+ for i, issue in enumerate(top10):
457
+ if i > 0 and issue["demand_score"] > top10[i-1]["demand_score"]:
458
+ failures.append("Top-10 list was not sorted by demand_score -- re-sorted.")
459
+ filtered.sort(key=lambda x: x["demand_score"], reverse=True)
460
+ top10 = filtered[:10]
461
+ break
462
+
463
+ # Verify: ignored demand issues meet all 3 criteria
464
+ ignored = [i for i in filtered if i["ignored_demand"]]
465
+ for issue in ignored:
466
+ if issue["reactions_plus1"] < 10 or issue["age_days"] < 180:
467
+ issue["ignored_demand"] = False
468
+ failures.append(f"Removed issue #{issue['number']} from ignored demand -- did not meet all 3 criteria")
469
+
470
+ # Check messaging brief counts
471
+ if len(brief.get("positioning_angles", [])) != 3:
472
+ failures.append(f"Expected 3 positioning angles, got {len(brief.get('positioning_angles', []))}")
473
+ if len(brief.get("outreach_hooks", [])) != 3:
474
+ failures.append(f"Expected 3 outreach hooks, got {len(brief.get('outreach_hooks', []))}")
475
+ if len(brief.get("cluster_headlines", [])) != 3:
476
+ failures.append(f"Expected 3 cluster headlines, got {len(brief.get('cluster_headlines', []))}")
477
+
478
+ # Check for em dashes in brief
479
+ brief_str = json.dumps(brief)
480
+ if "\u2014" in brief_str:
481
+ brief_str = brief_str.replace("\u2014", " - ")
482
+ brief = json.loads(brief_str)
483
+ failures.append("Fixed: em dash characters removed from messaging brief")
484
+
485
+ # Check for forbidden words
486
+ forbidden = ["powerful", "robust", "seamless", "innovative", "game-changing", "streamline", "leverage", "transform"]
487
+ full_text = (json.dumps(clusters) + json.dumps(brief)).lower()
488
+ for word in forbidden:
489
+ if word in full_text:
490
+ failures.append(f"Warning: forbidden word '{word}' found in output -- review before presenting")
491
+
492
+ # Build final output bundle
493
+ output = {
494
+ "repo": target,
495
+ "issues_analyzed": len(filtered),
496
+ "clusters": clusters,
497
+ "enriched_themes": themes,
498
+ "filtered_issues": filtered,
499
+ "messaging_brief": brief,
500
+ "data_quality_flags": failures
501
+ }
502
+
503
+ json.dump(output, open("/tmp/ghd-output.json", "w"), indent=2)
504
+ print(f"QA complete. Issues addressed: {len(failures)}")
505
+ for f in failures:
506
+ print(f" - {f}")
507
+ if not failures:
508
+ print("All QA checks passed.")
509
+ PYEOF
510
+ ```
511
+
512
+ ---
513
+
514
+ ## Step 8: Save and Present Output
515
+
516
+ ```bash
517
+ python3 << 'PYEOF'
518
+ import json
519
+ from datetime import datetime, timezone
520
+
521
+ output = json.load(open("/tmp/ghd-output.json"))
522
+ target = output["repo"]
523
+ repo_slug = target.replace("/", "-")
524
+ date_str = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
525
+
526
+ filtered = output["filtered_issues"]
527
+ themes = output["enriched_themes"]
528
+ clusters = output["clusters"]
529
+ brief = output["messaging_brief"]
530
+ flags = output["data_quality_flags"]
531
+
532
+ ignored = [i for i in filtered if i.get("ignored_demand")]
533
+ top10 = filtered[:10]
534
+
535
+ # Build category summary from cluster data
536
+ category_counts = clusters.get("category_counts", {})
537
+
538
+ lines = [
539
+ f"## Demand Gap Report: {target}",
540
+ f"Issues analyzed: {output['issues_analyzed']} | Date: {date_str}",
541
+ "",
542
+ "---",
543
+ "",
544
+ "### Demand Gap Leaderboard",
545
+ "",
546
+ "| Rank | Theme | Category | Issues | Total Demand Score | Top Issue Reactions |",
547
+ "|---|---|---|---|---|---|",
548
+ ]
549
+
550
+ for i, theme in enumerate(themes[:8], 1):
551
+ top_reactions = theme["top_issues"][0]["reactions"] if theme["top_issues"] else 0
552
+ lines.append(
553
+ f"| {i} | {theme['theme_name']} | {theme['category']} | "
554
+ f"{theme['issue_count']} | {theme['total_demand_score']} | {top_reactions} |"
555
+ )
556
+
557
+ lines += ["", "---", ""]
558
+
559
+ if ignored:
560
+ lines += [
561
+ "### Ignored Demand (High Reactions, No Maintainer Response)",
562
+ "",
563
+ "These issues have 10+ reactions, are 6+ months old, and have no planned/in-progress label.",
564
+ "This is your opportunity window.",
565
+ "",
566
+ ]
567
+ for issue in ignored[:10]:
568
+ lines.append(
569
+ f"- [{issue['title']}]({issue['url']}) -- "
570
+ f"{issue['reactions_plus1']} reactions, {issue['age_days']} days old"
571
+ )
572
+ lines += ["", "---", ""]
573
+
574
+ lines += [
575
+ "### Top 10 Highest-Demand Issues",
576
+ "",
577
+ "| Rank | Issue | Reactions | Comments | Demand Score | Link |",
578
+ "|---|---|---|---|---|---|",
579
+ ]
580
+ for i, issue in enumerate(top10, 1):
581
+ short_title = issue["title"][:70] + ("..." if len(issue["title"]) > 70 else "")
582
+ lines.append(
583
+ f"| {i} | {short_title} | {issue['reactions_plus1']} | "
584
+ f"{issue['comments']} | {round(issue['demand_score'], 1)} | "
585
+ f"[#{issue['number']}]({issue['url']}) |"
586
+ )
587
+
588
+ lines += ["", "---", "", "### Cluster Deep Dives", ""]
589
+
590
+ for theme in themes[:3]:
591
+ lines.append(f"#### {theme['theme_name']}")
592
+ lines.append(f"Category: {theme['category']} | Issues: {theme['issue_count']} | Total demand score: {theme['total_demand_score']}")
593
+ lines.append("")
594
+ lines.append("Top issues in this cluster:")
595
+ for ti in theme["top_issues"]:
596
+ lines.append(f"- \"{ti['title']}\" -- {ti['reactions']} reactions")
597
+ lines.append("")
598
+
599
+ lines += ["---", "", "### Messaging Brief", ""]
600
+
601
+ for angle in brief.get("positioning_angles", []):
602
+ lines.append(f"**{angle.get('angle_name', 'Angle')}**")
603
+ lines.append(angle.get("positioning_statement", ""))
604
+ lines.append(f"Evidence: \"{angle.get('evidence', '')}\"")
605
+ lines.append("")
606
+
607
+ lines += ["---", "", "### GTM Angles", ""]
608
+
609
+ for hook in brief.get("outreach_hooks", []):
610
+ lines.append(f"**{hook.get('hook_type', 'Hook')}**")
611
+ lines.append(hook.get("hook_text", ""))
612
+ lines.append(f"Best for: {hook.get('best_for', '')}")
613
+ lines.append("")
614
+
615
+ lines += ["---", ""]
616
+ if flags:
617
+ lines.append(f"Data quality notes: {'; '.join(flags)}")
618
+ else:
619
+ lines.append("Data quality notes: None")
620
+
621
+ output_path = f"docs/demand-signals/{repo_slug}-{date_str}.md"
622
+ import os
623
+ os.makedirs("docs/demand-signals", exist_ok=True)
624
+ open(output_path, "w").write("\n".join(lines))
625
+ print(f"Saved to: {output_path}")
626
+
627
+ # Print to console
628
+ print("\n" + "\n".join(lines))
629
+ PYEOF
630
+ ```
631
+
632
+ Clean up temp files:
633
+
634
+ ```bash
635
+ rm -f /tmp/ghd-target.txt /tmp/ghd-raw-issues.json /tmp/ghd-filtered-issues.json \
636
+ /tmp/ghd-cluster-request.json /tmp/ghd-clusters.json /tmp/ghd-enriched-themes.json \
637
+ /tmp/ghd-brief-request.json /tmp/ghd-brief.json /tmp/ghd-output.json
638
+ ```
@@ -0,0 +1,118 @@
1
+ [
2
+ {
3
+ "id": "eval_001",
4
+ "name": "Popular OSS repo: full pipeline with 100+ issues, all 6 categories populated",
5
+ "description": "A large, active public repo with hundreds of open issues. Validates the full 8-step workflow, noise filtering, demand scoring, all 6 categories appearing in output, and correct sorting.",
6
+ "input": {
7
+ "prompt": "Scan competitor GitHub issues: https://github.com/facebook/react",
8
+ "env": {
9
+ "GITHUB_TOKEN": "set"
10
+ }
11
+ },
12
+ "expected_behavior": [
13
+ "Parses owner=facebook, repo=react correctly",
14
+ "Fetches 2 pages of issues from GitHub REST API",
15
+ "Checks X-RateLimit-Remaining after first page fetch",
16
+ "Filters out pull requests (pull_request key present), bot-authored issues, chore/deps/bump title patterns, zero-reaction + zero-comment issues",
17
+ "Issue count after filtering is above 10 -- proceeds to clustering",
18
+ "demand_score computed as (reactions_plus1 * 2) + (comments * 0.5) for each issue",
19
+ "ignored_demand field set to true only for issues with reactions >= 10, age_days >= 180, and no planned label",
20
+ "Skill prints filtered issues and asks the AI to classify them into 6 categories",
21
+ "AI writes /tmp/ghd-clusters.json with classified_issues, cluster_themes, and category_counts",
22
+ "All 6 categories appear in category_counts",
23
+ "Top-10 list sorted by demand_score descending",
24
+ "Skill prints top 3 clusters and asks the AI to write a messaging brief",
25
+ "AI writes /tmp/ghd-brief.json with exactly 3 positioning_angles, 3 outreach_hooks, 3 cluster_headlines",
26
+ "Every outreach hook contains a quoted verbatim issue title",
27
+ "Output saved to docs/demand-signals/facebook-react-[date].md",
28
+ "No em dashes in any output section"
29
+ ],
30
+ "expected_output": "Full demand gap report with leaderboard, ignored demand section, top-10, cluster deep dives, messaging brief, and GTM angles"
31
+ },
32
+ {
33
+ "id": "eval_002",
34
+ "name": "Small or inactive repo: graceful stop after Step 4",
35
+ "description": "A repo with very few open issues or a niche project where most issues have zero reactions. After noise filtering, fewer than 10 issues remain. Validates graceful stop with a clear explanation.",
36
+ "input": {
37
+ "prompt": "Analyze issues in this repo: https://github.com/nicowillis/tiny-test-repo",
38
+ "env": {
39
+ "GITHUB_TOKEN": "set"
40
+ }
41
+ },
42
+ "expected_behavior": [
43
+ "Fetches issues successfully",
44
+ "Noise filter removes most issues (zero engagement, bot patterns, etc.)",
45
+ "After filtering, fewer than 10 issues remain",
46
+ "Stops at Step 4 -- does NOT proceed to clustering",
47
+ "Tells the user exactly how many issues were found and how many were filtered",
48
+ "Explains why fewer than 10 issues is insufficient for reliable clustering",
49
+ "Suggests trying a larger or more community-engaged repo",
50
+ "No partial output generated"
51
+ ],
52
+ "expected_output": "Graceful stop message with issue counts, noise breakdown, and suggestion to try a different repo"
53
+ },
54
+ {
55
+ "id": "eval_003",
56
+ "name": "Private or non-existent repo: stops at Step 3 with exact error",
57
+ "description": "User provides a URL for a repo that either does not exist (404) or is private (403). Validates that the skill stops at Step 3 with a clear, actionable error message.",
58
+ "input": {
59
+ "prompt": "Find demand gaps in https://github.com/nonexistent-org/nonexistent-repo-xyz",
60
+ "env": {
61
+ "GITHUB_TOKEN": "set"
62
+ }
63
+ },
64
+ "expected_behavior": [
65
+ "Parses owner/repo from URL",
66
+ "Attempts GitHub API fetch",
67
+ "GitHub returns 404",
68
+ "Stops immediately at Step 3",
69
+ "Tells the user: 'Repo not found. Check the URL or slug and try again. Private repos are not accessible without authentication and explicit repo scope.'",
70
+ "Does NOT attempt noise filtering",
71
+ "Does NOT proceed to clustering"
72
+ ],
73
+ "expected_output": "Immediate stop at Step 3 with 404 error message. No partial analysis generated."
74
+ },
75
+ {
76
+ "id": "eval_004",
77
+ "name": "owner/repo slug input (no full URL): parses correctly and runs full pipeline",
78
+ "description": "User provides an owner/repo slug instead of a full GitHub URL (e.g. 'vercel/next.js' instead of 'https://github.com/vercel/next.js'). Validates that the slug parsing path works correctly.",
79
+ "input": {
80
+ "prompt": "What are users asking for in vercel/next.js?",
81
+ "env": {
82
+ "GITHUB_TOKEN": "set"
83
+ }
84
+ },
85
+ "expected_behavior": [
86
+ "Step 2 detects no 'github.com' URL in the input",
87
+ "Falls into the owner/repo slug parsing path",
88
+ "Correctly splits 'vercel/next.js' into owner=vercel, repo=next.js",
89
+ "Writes 'vercel/next.js' to /tmp/ghd-target.txt",
90
+ "Step 3 fetches from https://api.github.com/repos/vercel/next.js/issues successfully",
91
+ "Full pipeline runs to completion",
92
+ "Output filename uses 'vercel-next.js' as the repo slug",
93
+ "No error about invalid URL format"
94
+ ],
95
+ "expected_output": "Full demand gap report identical in format to a full URL run. Slug parsing is transparent to the output."
96
+ },
97
+ {
98
+ "id": "eval_005",
99
+ "name": "No GITHUB_TOKEN, rate limit hit: stops with reset time and token instructions",
100
+ "description": "User has no GITHUB_TOKEN and has already hit the 60/hr unauthenticated rate limit. Validates that the skill detects the rate limit header and stops with the reset time and token setup instructions.",
101
+ "input": {
102
+ "prompt": "What are users asking for in facebook/react?",
103
+ "env": {
104
+ "GITHUB_TOKEN": "not set"
105
+ }
106
+ },
107
+ "expected_behavior": [
108
+ "Step 1 notes GITHUB_TOKEN is not set and warns about 60 req/hr limit",
109
+ "Step 3 attempts GitHub API fetch without token",
110
+ "GitHub API returns X-RateLimit-Remaining: 0 header",
111
+ "Skill detects the header value after the first page fetch",
112
+ "Stops immediately with message: exact reset time (converted from X-RateLimit-Reset timestamp) and instructions to add GITHUB_TOKEN at github.com/settings/tokens",
113
+ "Does NOT attempt page 2 fetch",
114
+ "Does NOT proceed to noise filtering or clustering"
115
+ ],
116
+ "expected_output": "Stop at Step 3 with rate limit message, reset time, and GitHub token setup link. No analysis generated."
117
+ }
118
+ ]
@@ -0,0 +1,181 @@
1
+ # Demand Categories Reference
2
+
3
+ Used by SKILL.md Step 5 to guide AI classification of GitHub issues into one of 6 demand categories.
4
+
5
+ ---
6
+
7
+ ## The 6 Categories
8
+
9
+ ### feature_gap
10
+
11
+ **What it captures:** Functionality the product does not have yet. User is describing something they want to do that is currently impossible.
12
+
13
+ **Signal phrases:**
14
+ - "add support for..."
15
+ - "it would be great if..."
16
+ - "please add..."
17
+ - "feature request:"
18
+ - "allow users to..."
19
+ - "I wish I could..."
20
+ - "would love to see..."
21
+
22
+ **Examples:**
23
+ - "Add support for keyboard shortcuts in the editor"
24
+ - "Allow exporting to PDF format"
25
+ - "Feature request: dark mode"
26
+ - "Support for multiple workspaces per account"
27
+
28
+ **Scoring note:** feature_gap issues with 20+ reactions represent product roadmap signals. These are the gaps your product can claim as intentional design choices.
29
+
30
+ ---
31
+
32
+ ### bug_pattern
33
+
34
+ **What it captures:** Recurring broken behavior that erodes user trust. Distinct from a one-off error -- the word "pattern" matters. Multiple issues with similar titles indicate a systemic problem.
35
+
36
+ **Signal phrases:**
37
+ - "broken when..."
38
+ - "not working..."
39
+ - "fails to..."
40
+ - "error when..."
41
+ - "crashes if..."
42
+ - "regression in..."
43
+ - "breaks after..."
44
+
45
+ **Examples:**
46
+ - "Login fails when using SSO with Google"
47
+ - "File upload crashes on files over 10MB"
48
+ - "Pagination breaks on mobile"
49
+ - "Notifications not sending after the latest update"
50
+
51
+ **Scoring note:** Bug patterns with high reactions signal trust erosion. If a competitor has 5+ high-reaction bug issues in the same functional area, that area is a liability in their product positioning.
52
+
53
+ ---
54
+
55
+ ### ux_complaint
56
+
57
+ **What it captures:** Friction, confusion, or workflow problems. The feature exists but it is hard to use, hard to find, or does not match how users actually work.
58
+
59
+ **Signal phrases:**
60
+ - "confusing..."
61
+ - "hard to..."
62
+ - "unclear how to..."
63
+ - "should be easier to..."
64
+ - "the UI for X is..."
65
+ - "annoying that..."
66
+ - "clunky..."
67
+ - "why does X require..."
68
+
69
+ **Examples:**
70
+ - "Confusing navigation between projects"
71
+ - "Hard to find the settings for notifications"
72
+ - "Should be easier to bulk-edit items"
73
+ - "The import flow has too many steps"
74
+
75
+ **Scoring note:** UX complaints are positioning gold. "Confusing to use" is a contrast you can own directly. If their users are calling something confusing, your messaging can address that exact friction without naming the competitor.
76
+
77
+ ---
78
+
79
+ ### performance
80
+
81
+ **What it captures:** Slowness, timeouts, resource usage, or reliability problems that degrade the experience even when the feature works correctly.
82
+
83
+ **Signal phrases:**
84
+ - "slow..."
85
+ - "timeout..."
86
+ - "takes too long..."
87
+ - "high memory usage..."
88
+ - "performance regression..."
89
+ - "loading forever..."
90
+ - "lags when..."
91
+ - "CPU usage..."
92
+
93
+ **Examples:**
94
+ - "Search is slow on repos with 1000+ files"
95
+ - "Dashboard takes 10+ seconds to load"
96
+ - "Memory usage spikes when processing large files"
97
+ - "Build times increased 3x after v2.0"
98
+
99
+ **Scoring note:** Performance issues cluster by data size or scale. If the complaints mention large repos, large teams, or high-volume usage, the competitor has a scale ceiling. That ceiling is your advantage if you have solved it.
100
+
101
+ ---
102
+
103
+ ### integration_missing
104
+
105
+ **What it captures:** Requests to connect with other tools, APIs, or platforms. Users want the product to work alongside something else in their stack.
106
+
107
+ **Signal phrases:**
108
+ - "integrate with..."
109
+ - "support for [tool name]..."
110
+ - "webhook..."
111
+ - "API for..."
112
+ - "connect to..."
113
+ - "import from..."
114
+ - "sync with..."
115
+ - "plugin for..."
116
+
117
+ **Examples:**
118
+ - "Integrate with Slack for notifications"
119
+ - "Support GitHub Actions webhook triggers"
120
+ - "Add Zapier integration"
121
+ - "Import from Notion"
122
+ - "VS Code extension"
123
+
124
+ **Scoring note:** Integration requests cluster around the tools their users already use. A high-reaction integration request tells you where their users spend the rest of their day. If you already have that integration, it is a direct switch argument.
125
+
126
+ ---
127
+
128
+ ### docs_missing
129
+
130
+ **What it captures:** Confusion caused by absent, incomplete, or incorrect documentation. The product may work correctly, but users cannot figure out how to use it.
131
+
132
+ **Signal phrases:**
133
+ - "no documentation for..."
134
+ - "docs are missing..."
135
+ - "unclear how to..."
136
+ - "example for..."
137
+ - "how do I..."
138
+ - "docs don't explain..."
139
+ - "add docs for..."
140
+ - "tutorial for..."
141
+
142
+ **Examples:**
143
+ - "No documentation for the webhook authentication flow"
144
+ - "Missing example for advanced configuration"
145
+ - "How do I set up custom domains? Docs don't cover this."
146
+ - "Add a tutorial for migrating from v1 to v2"
147
+
148
+ **Scoring note:** docs_missing issues often indicate a product that has grown faster than its documentation. High-reaction docs issues in a specific area indicate that the area is both important to users and opaque in practice.
149
+
150
+ ---
151
+
152
+ ## Classification Rules
153
+
154
+ ### One category per issue
155
+ Every issue gets exactly one category. Use the primary pain, not all possible interpretations.
156
+
157
+ - "The export feature is broken and also slow" -- classify as `bug_pattern` (primary pain: it does not work)
158
+ - "Export to PDF is slow" -- classify as `performance` (it works, it is just slow)
159
+ - "Add export to PDF" -- classify as `feature_gap` (it does not exist)
160
+
161
+ ### When to use ux_complaint vs docs_missing
162
+ - If the user says "I can't figure out how to X" and the docs don't cover it: `docs_missing`
163
+ - If the user says "X is confusing" or "X is hard to use" and the feature exists: `ux_complaint`
164
+ - If both apply: `ux_complaint` (the UI is the product, the docs are secondary)
165
+
166
+ ### When to use bug_pattern vs performance
167
+ - If it does not work at all: `bug_pattern`
168
+ - If it works but is slow or resource-heavy: `performance`
169
+
170
+ ---
171
+
172
+ ## Category Demand Signal Interpretation
173
+
174
+ | Category | What high demand here tells you |
175
+ |---|---|
176
+ | feature_gap | Their roadmap is behind their users. Name what you have built. |
177
+ | bug_pattern | Trust erosion in a specific area. Position your reliability there. |
178
+ | ux_complaint | Their users are struggling. Position your simplicity there. |
179
+ | performance | They hit a scale ceiling. Position your throughput or response time. |
180
+ | integration_missing | Their users live in a different stack. Show your integration depth. |
181
+ | docs_missing | They ship but do not explain. Position your onboarding and support. |
@@ -0,0 +1,211 @@
1
+ # GTM Translation Reference
2
+
3
+ How to convert demand clusters from competitor GitHub issues into GTM language. Used by SKILL.md Step 6 to guide the messaging brief generation.
4
+
5
+ ---
6
+
7
+ ## The Translation Problem
8
+
9
+ Raw demand data from GitHub is in user language, not buyer language. A product manager reads:
10
+
11
+ > "Add support for multiple workspaces per account" -- 87 reactions, open 2 years
12
+
13
+ A GTM-trained analyst reads:
14
+
15
+ > "Enterprise buyers need multi-tenancy. This competitor has not shipped it in 2 years. 87 teams are waiting. If you have this, lead with it."
16
+
17
+ This reference document is the bridge between those two readings.
18
+
19
+ ---
20
+
21
+ ## Translation by Category
22
+
23
+ ### feature_gap clusters
24
+
25
+ **What the data shows:** Users want something that does not exist.
26
+
27
+ **GTM translation:**
28
+ - If you have the feature: "We built what [competitor] has not." Name the feature specifically.
29
+ - If you are building it: Use the cluster as proof that the market exists. "87 teams on [competitor] asked for X. We built it."
30
+ - If you do not have it: This is a roadmap signal, not a positioning signal yet.
31
+
32
+ **Outreach hook pattern:**
33
+ ```
34
+ "[Number] teams on [competitor] have been asking for [specific feature] for [time].
35
+ We shipped [your feature] in [your product] [time ago / as a core feature].
36
+ [One-line ask]."
37
+ ```
38
+
39
+ **Example:**
40
+ ```
41
+ "Over 200 teams on [competitor] asked for multi-workspace support. It has been open for 3 years with no planned label.
42
+ We built workspace isolation as a core feature, not an add-on.
43
+ Would a 20-minute call be useful?"
44
+ ```
45
+
46
+ ---
47
+
48
+ ### bug_pattern clusters
49
+
50
+ **What the data shows:** Something is reliably broken. High reaction counts on bug issues mean many users hit the same wall.
51
+
52
+ **GTM translation:**
53
+ - Do not say "they are buggy." Say: "Teams in [X workflow] need reliability."
54
+ - Position the broken area as a category where you have invested specifically.
55
+ - The number of reactions is the credibility anchor. Use it.
56
+
57
+ **Outreach hook pattern:**
58
+ ```
59
+ "[Number] teams using [competitor] hit [specific bug area].
60
+ [Your product] was built [for/around/specifically to handle] this workflow without [the failure mode].
61
+ [One-line ask]."
62
+ ```
63
+
64
+ **Example:**
65
+ ```
66
+ "Teams using [competitor] have hit login failures with SSO -- 43 reactions on that issue, open for 18 months.
67
+ We built our auth layer around SSO-first design and have not had an SSO outage in 18 months of production.
68
+ Worth a conversation?"
69
+ ```
70
+
71
+ ---
72
+
73
+ ### ux_complaint clusters
74
+
75
+ **What the data shows:** The feature exists but users cannot use it without friction.
76
+
77
+ **GTM translation:**
78
+ - Simplicity is a product decision, not a feature. Position it as intentional design.
79
+ - Use "setup time" or "time to value" as the measurable proxy for simplicity.
80
+ - Do not say "our UI is better." Say: "Teams get to [outcome] in [time], without [the friction they described]."
81
+
82
+ **Outreach hook pattern:**
83
+ ```
84
+ "[Number] users of [competitor] said [specific UX friction in quotes].
85
+ [Your product] gets [persona] to [outcome] in [time] with [fewer steps / no setup / no configuration].
86
+ [One-line ask]."
87
+ ```
88
+
89
+ **Example:**
90
+ ```
91
+ "158 users of [competitor] called the project navigation 'confusing' -- that issue is 2 years old.
92
+ We redesigned navigation around the workflow, not the feature tree. Teams reach their first result in under 2 minutes.
93
+ Would you like to see it?"
94
+ ```
95
+
96
+ ---
97
+
98
+ ### performance clusters
99
+
100
+ **What the data shows:** The product hits a scale ceiling. High-reaction performance issues cluster around a specific bottleneck (large files, large teams, high query volume).
101
+
102
+ **GTM translation:**
103
+ - Name the scale ceiling specifically: "repos over 1000 files", "teams over 50 users", "queries over 10k/day".
104
+ - If you have benchmarks, use them. If not, use the absence of the complaint as proof.
105
+ - Performance positioning works best for technical buyers (engineers, infrastructure teams).
106
+
107
+ **Outreach hook pattern:**
108
+ ```
109
+ "Teams at [competitor] hit [specific bottleneck] at [scale threshold].
110
+ [Your product] handles [scale threshold] in [benchmark or time].
111
+ [One-line ask]."
112
+ ```
113
+
114
+ **Example:**
115
+ ```
116
+ "Engineering teams using [competitor] report 10+ second load times on repos over 500 files -- 67 reactions, no fix in sight.
117
+ We load repos of that size in under 1.5 seconds because we index differently.
118
+ I can show you a benchmark on a repo your size."
119
+ ```
120
+
121
+ ---
122
+
123
+ ### integration_missing clusters
124
+
125
+ **What the data shows:** Users work in tools this competitor does not connect to. High-reaction integration requests tell you exactly what else is in their users' daily stack.
126
+
127
+ **GTM translation:**
128
+ - If you have the integration: make it the lead, not a footnote.
129
+ - The reaction count is the size of the audience that wants this bridge. Name the number.
130
+ - Integration positioning works best when the target tool is a category leader (Slack, Notion, GitHub Actions, Salesforce).
131
+
132
+ **Outreach hook pattern:**
133
+ ```
134
+ "[Number] teams on [competitor] asked for [specific integration]. It has been open [time] with no planned label.
135
+ [Your product] connects to [integration] natively -- [brief description of how it works].
136
+ [One-line ask]."
137
+ ```
138
+
139
+ **Example:**
140
+ ```
141
+ "94 teams on [competitor] asked for Slack integration. The request is 3 years old with no planned label.
142
+ We shipped bidirectional Slack sync 6 months ago -- alerts in Slack, actions back to [your product].
143
+ Worth showing you?"
144
+ ```
145
+
146
+ ---
147
+
148
+ ### docs_missing clusters
149
+
150
+ **What the data shows:** Users cannot figure out how to do something. The product may work, but the path to value is opaque.
151
+
152
+ **GTM translation:**
153
+ - Documentation is a trust signal, not a feature. "We have clear docs" is not a hook.
154
+ - Translate to: "Teams are in production in [time]" or "We have a guided setup for [exact use case they documented badly]."
155
+ - Onboarding speed is the business outcome. Use it.
156
+
157
+ **Outreach hook pattern:**
158
+ ```
159
+ "[Number] users on [competitor] asked for documentation on [specific topic].
160
+ We have [specific guide / template / interactive tutorial] for [exact use case].
161
+ Teams using our [guide/setup] are in production in [time].
162
+ [One-line ask]."
163
+ ```
164
+
165
+ **Example:**
166
+ ```
167
+ "38 teams on [competitor] asked for documentation on webhook authentication. There is still no official guide.
168
+ We have a step-by-step webhook setup guide that gets teams from zero to first event in under 15 minutes.
169
+ I can send it -- it works for any webhook destination, not just ours."
170
+ ```
171
+
172
+ ---
173
+
174
+ ## Using Verbatim Issue Language
175
+
176
+ The most effective outreach hooks use the user's exact words from the issue title. This works because:
177
+
178
+ 1. It proves you read real feedback, not marketing copy.
179
+ 2. It speaks in the buyer's language, not product language.
180
+ 3. It names the pain before pitching the solution.
181
+
182
+ **Wrong approach (paraphrased):**
183
+ > "Many teams find navigation confusing in competitor products."
184
+
185
+ **Right approach (verbatim quote):**
186
+ > "158 users said 'confusing navigation between projects' has been open for 2 years. We redesigned for workflow, not feature hierarchy."
187
+
188
+ The quote is the credibility anchor. Do not remove it.
189
+
190
+ ---
191
+
192
+ ## Ignored Demand: The Highest-Signal Category
193
+
194
+ An issue that meets all three criteria:
195
+ - 10+ reactions (significant real demand)
196
+ - Open 180+ days (competitor has had time to act)
197
+ - No planned/in-progress label (they have explicitly not prioritized it)
198
+
199
+ This is not a feature request. This is a documented unmet need with a timestamp.
200
+
201
+ **GTM translation:** "Their users asked. Their team did not respond. We built it."
202
+
203
+ This framing works across all 6 categories. The combination of volume + age + inaction is the signal. The category determines the messaging angle.
204
+
205
+ **Ignored demand outreach template:**
206
+ ```
207
+ "[Competitor]'s users have been asking for [verbatim issue title] for [age].
208
+ [Reaction count] teams upvoted it. There is no planned label.
209
+ [Your product] handles this [natively / differently / as a core feature].
210
+ [One-line ask]."
211
+ ```