Skip to content
Marketing
Skill

/gh-issue-to-demand-signal

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.

From plugin
opendirectory-gtm-skills
58364 skills
Install
$ npx -y skills add Varnan-Tech/opendirectory --skill gh-issue-to-demand-signal --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/gh-issue-to-demand-signal

Context preview

The summary Claude sees to decide when to auto-load this skill.

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.

SKILL.md

gh-issue-to-demand-signal.SKILL.md
name: gh-issue-to-demand-signal
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".
compatibility: [claude-code, gemini-cli, github-copilot]

GitHub Issue Demand Signal

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.

---

**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.

---

Common Mistakes

| The agent will want to... | Why that's wrong | |---|---| | 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. | | Use comment count as the primary demand signal | Comments include maintainer responses, off-topic discussion, and spam. reactions["+1"] is the cleanest buyer signal. | | Paraphrase issue titles when summarizing clusters | Paraphrasing loses the buyer's exact language, which is the entire point. Use verbatim issue titles. | | 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. | | 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. | | 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. |

---

Step 1: Setup Check

echo "GITHUB_TOKEN: ${GITHUB_TOKEN:-not set, unauthenticated rate limit applies (60 req/hr)}"

**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)."

---

Step 2: Gather Input

You need:

  • GitHub repo URL (e.g. https://github.com/owner/repo) or owner/repo slug (e.g. facebook/react)

Parse owner and repo from input:

python3 << 'PYEOF'
import re, sys, os

raw = "REPO_INPUT_HERE"

# Normalize to owner/repo
if raw.startswith("http"):
    m = re.search(r"github\.com/([^/]+)/([^/?\s]+)", raw)
    if not m:
        print("ERROR: Could not parse GitHub URL. Expected format: https://github.com/owner/repo")
        sys.exit(1)
    owner, repo = m.group(1), m.group(2).rstrip("/")
elif "/" in raw:
    parts = raw.strip().split("/")
    owner, repo = parts[0], parts[1]
else:
    print("ERROR: Input must be a GitHub URL or owner/repo slug (e.g. vercel/next.js)")
    sys.exit(1)

print(f"Owner: {owner}")
print(f"Repo: {repo}")

with open("/tmp/ghd-target.txt", "w") as f:
    f.write(f"{owner}/{repo}")
PYEOF

**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)."

---

Step 3: Fetch Issues from GitHub REST API

Fetch up to 200 issues (2 pages of 100). Check rate limit after the first fetch.

python3 << 'PYEOF'
import json, urllib.request, os, sys
from datetime import datetime, timezone

target = open("/tmp/ghd-target.txt").read().strip()
owner_repo = target
token = os.environ.get("GITHUB_TOKEN", "")

headers = {"Accept": "application/vnd.github+json", "User-Agent": "gh-issue-demand-signal/1.0"}
if token:
    headers["Authorization"] = f"Bearer {token}"

all_issues = []
rate_limit_hit = False

for page in [1, 2]:
    url = f"https://api.github.com/repos/{owner_repo}/issues?state=open&per_page=100&page={page}"
    req = urllib.request.Request(url, headers=headers)

    try:
        with urllib.request.urlopen(req, timeout=30) as resp:
            # Check rate limit after first page
            if page == 1:
                remaining = int(resp.headers.get("X-RateLimit-Remaining", 999))
                reset_ts = resp.headers.get("X-RateLimit-Reset", "")
                if remaining == 0:
                    reset_str = datetime.fromtimestamp(int(reset_ts), tz=timezone.utc).strftime("%H:%M UTC") if reset_ts else "unknown"
                    print(f"ERROR: GitHub rate limit exhausted. Resets at {reset_str}.")
                    print("Add GITHUB_TOKEN to your .env file to get 5000 req/hr. See github.com/settings/tokens (no scopes needed).")
                    sys.exit(1)
                print(f"Rate limit remaining: {remaining}")

                # Check for 404/403
                status = resp.status
                if status == 404:
                    print(f"ERROR: Repo '{owner_repo}' not found. Check the URL or slug.")
                    sys.exit(1)

            page_data = json.loads(resp.read())
            if not page_data:
                print(f"Page {page}: empty, stopping.")
                break
            all_issues.extend(page_data)
            print(f"Page {page}: {len(page_data)} issues fe
Read more
Ships withopendirectory-gtm-skills

AI Agent Skills built for Founders who hate Marketing

Get the whole plugin