Skip to content
cccskills
FreePublished on GitHub

should-i-contribute

Research any open-source repository end to end and answer the core question — SHOULD YOU contribute to it? — with a GO / NO-GO / GO-IF verdict backed by evidence: snapshot, momentum, contribution landscape (PR acceptance rates, outside-contributor merge rates, first-timer outcomes), issue health, governance, community sentiment, and a final go/no-go recommendation. Use when the user asks 'should I contribute to X', wants a before-I-contribute due-diligence check on a project, asks whether a project accepts outside PRs, wants contribution-health numbers like PR merge rates or stale-PR counts, or asks for a deep dive on an open-source codebase's health and dynamics before investing time in it.

The English summary is being prepared. Showing the original description.

Run score Not tested

See how to install

Files included (4)

  • SKILL.md23.6 KB
  • .gitignore35 B
  • LICENSE1.0 KB
  • README.md5.5 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

Should I Contribute? — repository contribution research

Research ANY open-source repository and produce a detailed analysis report: what it is, how healthy it is, how it actually treats outside contributors, what a PR/issue from a stranger would experience — and, at the end, a clear GO / NO-GO / GO-IF verdict on whether YOU should contribute. The core insight: PR acceptance is a much better signal of open-source health than PR volume — a project can look busy while quietly ignoring everyone outside the core team.

Output contract

Deliver ONE markdown report with these sections (see Report template at the end):

  1. Executive summary — 3-6 bullet verdict (GO / GO-IF / NO-GO) with the headline numbers
  2. Snapshot — identity, stats, license, stack
  3. Momentum — is the project alive and shipping?
  4. Contribution landscape — the core: open-PR census, age buckets, outside-author share, merge rates, first-timer outcomes
  5. Issue health — open issues, stale bugs, "documented bug with unmerged fix" search
  6. Governance & maintainability — docs, templates, who actually merges, bus factor
  7. Community sentiment — HN/Reddit/Discord signals, maintainer quotes
  8. Risks & opportunities for the requester
  9. Methodology & caveats — every number gets its source and sample

Every claim MUST be grounded in a number you computed or a quote you read. No vibes.

Setup

Target and API access (GitHub, the common case)

  • ALWAYS set the target explicitly first: REPO="owner/name" (e.g. REPO="anomalyco/opencode"). Use $REPO in every request. Do NOT rely on {owner}/{repo} placeholders — gh only expands those inside a cloned repo or with -R.
  • gh CLI authenticated → 5,000 req/hr. USE IT if available: gh api "repos/$REPO/...". If gh is not authenticated, gh auth login or fall back to curl with a token: curl -H "Authorization: Bearer $GITHUB_TOKEN" "https://api.github.com/repos/$REPO/...".
  • Raw unauthenticated curl → 60 req/hr core, 10/min search. The full analysis needs ~20-40 requests. Budget them; do NOT repeat paginated calls wastefully. If you hit 403 rate-limit, authenticate or shrink the sample.
  • Always request per_page=100 (max). Paginate via the Link: rel="next" header or &page=N. Stop when a page returns fewer than per_page items.
  • Renamed/transferred repos: if a search query 422s with "cannot be searched", the repo was likely renamed — GitHub search doesn't index redirect targets. Resolve the canonical name first: gh api "repos/$REPO" --jq .full_name (e.g. sst/opencode → anomalyco/opencode) and redo all queries with it.

Non-GitHub hosts

  • GitLab: https://gitlab.com/api/v4/projects/<url-encoded-path> — mirrors: merge_requests?state=opened&per_page=100, issues, releases; MR merged state: state=merged; author identity via author.username vs members/all (auth required).
  • Self-hosted / no API: shallow clone (git clone --depth=1) and analyze git log — you lose PR-level data; say so in the report.

Phase 1 — Snapshot

REPO="owner/name"
gh api "repos/$REPO" --jq '{name, description, language, license: (.license.spdx_id // null), stars: .stargazers_count, forks: .forks_count, open_issues: .open_issues_count, archived, pushed_at, created_at, homepage, topics, default_branch, visibility}'
gh api "repos/$REPO/languages" --jq 'to_entries | sort_by(-.value) | .[:6]'
gh api "repos/$REPO/community/profile" --jq '{health_percentage, files: (.files | with_entries(.value = (.value | type == "object"))) }'

Community profile is gold: it reveals whether CONTRIBUTING.md, CODE_OF_CONDUCT, issue/PR templates, README exist — the on-ramp for outsiders. Note pushed_at (last commit to default branch).

Read README.md (top ~80 lines) and CONTRIBUTING.md if present — verbatim quotes from these go in the report. Extract: what is it, who is it for, install/usage, contribution workflow promised vs actual (later phases measure the gap).

Phase 2 — Momentum

# 52 weeks of commit counts
gh api "repos/$REPO/stats/commit_activity" --jq '[.[] | .total] | {sum: add, weeks_with_commits: (map(select(. > 0)) | length), last_8_weeks: .[-8:]}'
# Releases (cadence + recency)
gh api "repos/$REPO/releases?per_page=30" --jq '.[] | {tag_name, published_at}'
# Contributor distribution (bus factor: how concentrated is the work?)
# PAGINATE ALL PAGES (per_page=100, page 1..N until empty) — page 1 alone is NOT the contributor base.
gh api "repos/$REPO/contributors?per_page=100&page=1"
# then accumulate; compute: {total: length, top5, top1_share: .[0].contributions / (map(.contributions)|add) * 100}
# Budget fallback: if >10 pages and rate-limited, label the stat "of the first N contributors" — never present as total.

Interpretation:

  • 6+ months of commits every week + releases in the last 3 months → alive. Watchdog: release gap > 6 months on a popular repo = zombie or big-bang rewrite incoming.
  • top-1 contributor > 40% of commits → bus-factor 1; contributions carry personal-review risk.
  • Read the last 3 release notes briefly (breaking changes, direction).

Phase 3 — Contribution landscape (THE core)

Compute ALL of these — they are the heart of the report.

3a. Open-PR census

# All open PRs: paginate per_page=100, page=1,2,... until an empty page; accumulate into one JSON array.
gh api "repos/$REPO/pulls?state=open&per_page=100&page=1"
# Cross-check the accumulated total against search (exact total_count):
gh api "search/issues?q=repo:$REPO+is:pr+is:open&per_page=1" --jq '.total_count'
# Fields per PR: number, title, author_association, created_at, user.login, draft, merged_at (null), labels

Compute, from the FULL open set:

  • total open PRs
  • outside-core share: count author_association in NONE | FIRST_TIMER | FIRST_TIME_CONTRIBUTOR | CONTRIBUTOR vs MEMBER | OWNER | COLLABORATOR
  • age buckets: open longer than 30 days, 90 days, 180 days (from created_at)
  • stale queue: open PRs with no maintainer activity in 30d (approximation: no timeline/review comments; if too costly, report age buckets only and say so)
  • draft PR share

3b. Closed-PR merge rate (exact, sampled on CLOSED-AT)

WARNING: pulls?state=closed sorts by created_at (desc), not closed_at — fetching its pages gives "most recently CREATED closed PRs", NOT "last closed". Never call that sample "last N closed".

EXACT method — partition the window into day (or week) slices, fetch EVERY result in each slice, dedupe, aggregate. Ordering becomes irrelevant and the 1,000-result search cap is avoided. This is the tested implementation (run via python3):

import json, subprocess, datetime
from collections import Counter
repo = "owner/name"; days = 90                      # <-- set these
end = datetime.date.today(); start = end - datetime.timedelta(days=days)

def iso(d):  # day-only bounds ("closed:A..B") are INCLUSIVE of both full days and span B-A+1 days —
    return d.strftime("%Y-%m-%dT00:00:00Z")  # use explicit hour bounds for a true half-open [A, B)

def search(q, page=1):
    url = f"search/issues?q={q}&per_page=100&page={page}"
    out = subprocess.run(["gh", "api", url], capture_output=True, text=True)
    if out.returncode != 0:
        raise RuntimeError(out.stderr[:300])
    return json.loads(out.stdout)

items, seen = [], set()
for d in range(days):
    s = start + datetime.timedelta(days=d)
    q = f"repo:{repo}+is:pr+is:closed+closed:{iso(s)}..{iso(s + datetime.timedelta(days=1))}"
    r = search(q)                                    # page 1 doubles as the total_count probe
    total = r["total_count"]
    assert total <= 1000, f"slice {s} too large ({total}) — split it by hour and recurse"
    page = 1
    while True:
        its = r.get("items", []) if page == 1 else search(q, page).get("items", [])
        for it in its:
            if it["number"] not in seen:
                seen.add(it["number"]); items.append(it)
        if len(its) < 100: break
        page += 1
        r = None

# INVARIANT: fetched population must equal the aggregate closed: total_count.
# Both use half-open [start 00:00Z, end 00:00Z) — exactly `days` full days (through
# yesterday). If they differ, there is a slice gap — do NOT report rates until it matches.
last = end - datetime.timedelta(days=1)
agg = search(f"repo:{repo}+is:pr+is:closed+closed:{iso(start)}..{iso(end)}")["total_count"]
assert len(items) == agg, f"fetched {len(items)} != aggregate {agg} — gap in slicing, abort rates"

merged = [i for i in items if i.get("pull_request", {}).get("merged_at")]
if items:
    print(f"window {start}..{last}: n={len(items)} merged={len(merged)} rate={len(merged)/len(items)*100:.1f}%")
else:
    print(f"window {start}..{last}: n=0 (no PRs closed in window)")
# Funnel cross-tab (3d): merged vs closed-unmerged per author_association bucket.
tab = Counter((i["author_association"], "merged" if i.get("pull_request", {}).get("merged_at") else "unmerged") for i in items)
for (assoc, outcome), n in sorted(tab.items()):
    print(f"{assoc:24s} {outcome:9s} {n}")

Bash-only alternative for the per-day iteration (GNU date): NEXT=$(date -d "$D +1 day" +%F) — never string-concatenate dates. Also fetch the exact aggregate counts and require them to match your accumulation (same invariant; define the window FIRST). NOTE: closed:A..B with day-only bounds is INCLUSIVE of both full days (spans B-A+1 days) — use explicit T00:00:00Z hour bounds for half-open windows:

REPO="owner/name"; START=$(date -d "90 days ago" +%Y-%m-%d); END=$(date -d yesterday +%Y-%m-%d)
# per-day slice: closed:${D}T00:00:00Z..$(date -d "$D +1 day" +%Y-%m-%d)T00:00:00Z
# aggregates over the same half-open [START 00:00Z, today 00:00Z) window:
gh api "search/issues?q=repo:$REPO+is:pr+is:closed+closed:${START}T00:00:00Z..$(date -d "$END +1 day" +%Y-%m-%d)T00:00:00Z&per_page=1" --jq '.total_count'
gh api "search/issues?q=repo:$REPO+is:pr+is:merged+closed:${START}T00:00:00Z..$(date -d "$END +1 day" +%Y-%m-%d)T00:00:00Z&per_page=1" --jq '.total_count'

Compute (state window and accumulated n, e.g. "all 262 PRs closed 2026-08-04..08-10"):

  • merged vs closed-unmerged counts (cross-check with the two total_counts above)
  • overall merge rate = merged / sample
  • outside-author merge rate: merged where author_association is outside (NONE/CONTRIBUTOR/FIRST_*)
  • core-author merge rate for contrast (MEMBER/OWNER/COLLABORATOR)
  • first-timer outcomes: of closed-unmerged, how many were FIRST_TIME_CONTRIBUTOR/FIRST_TIMER? Of merged, how many?
  • time-window drill-down (monthly trick): last fully-elapsed month, merged count and outside merges that month. Derive the month dynamically — never hardcode it:
LAST_MONTH=$(date -d "$(date +%Y-%m-01) -1 day" +%Y-%m)          # last completed month (GNU date; macOS: date -v1d -v-1d +%Y-%m)
START="${LAST_MONTH}-01"; END=$(date -d "${LAST_MONTH}-01 +1 month -1 day" +%Y-%m-%d)
gh api "search/issues?q=repo:$REPO+is:pr+is:merged+merged:${START}..${END}&per_page=1" --jq '.total_count'
gh api "search/issues?q=repo:$REPO+is:pr+is:closed+closed:${START}..${END}&per_page=1" --jq '.total_count'

These totals alone cannot split outside vs core merges — the split needs per-item data, so run the 3b day-slice loop over the month window (or merged:${START}..${END} items) and classify by author_association. State in Methodology which you did.

Budget reality check: full 90-day chunking ≈ 90+ requests (1 per day slice, plus one per extra 100-item page) — fine with authed gh (5,000/hr); with unauth curl (60/hr) shrink the window to 7-14 days (fewer slices, exact within it) and LABEL it: "exact for the last N days only". Only if rate limits make even that impossible, fall back to pulls?state=closed pages labeled "most recently CREATED closed PRs — created-order approximation, NOT last closed".

Never sample with sort=updated / sort=created on search — ordering there is relevance/activity-based and is empirically unusable for rates (measured: 0/100 merged by updated desc in a window whose true rate was 41%).

3c. Author-association decoding

GitHub's author_association field on PRs/issues:

ValueMeaning
OWNERrepo owner
MEMBERorg member / core team
COLLABORATORexplicitly added collaborator
CONTRIBUTORhas a prior merged PR
FIRST_TIME_CONTRIBUTORfirst PR on the repo (has committed before)
FIRST_TIMERvery first GitHub interaction
NONEnever engaged

Outside/core split for the report: outside = NONE + FIRST_TIMER + FIRST_TIME_CONTRIBUTOR + CONTRIBUTOR; core = MEMBER + OWNER + COLLABORATOR. State the split in the report's Methodology. Caveat: COLLABORATOR can include invited community members — check repo collaborators when the count is material.

3d. The first-timer funnel

The most damning number: of PRs closed WITHOUT merging, how many were first-time contributors.

GitHub search does NOT support author_association: as a qualifier (it 422s), but search result items carry author_association — so compute the funnel from the slice-accumulated items of 3b (deduped by number, classified via .pull_request.merged_at). The 3b Python already prints this cross-tab; if you accumulated items in a JSON file instead, aggregate with:

# items.json = accumulated 3b items. Merged status MUST come from .pull_request.merged_at (top-level is null in search results).
jq -r 'group_by(.author_association)[] | "\(.[0].author_association): total=\(length) merged=\(map(select(.pull_request.merged_at != null)) | length)"' items.json

If you need a bigger funnel population, extend the window with more day slices (3b step 2).

Report: "of N closed-unmerged PRs in the window, M were by first-timers (FIRST_TIMER/FIRST_TIME_CONTRIBUTOR); of the merged ones, K were first-timers." Also check the 20 most recently MERGED PRs' authors (from the 3b items, sort by .pull_request.merged_at descending — no other ordering is valid): any outside names repeated? Repetition = pipeline actually works for newcomers.

Phase 4 — Issue health

# Exact open-issue total (search total_count is exact; issue items are capped at 1000):
gh api "search/issues?q=repo:$REPO+is:issue+is:open&per_page=1" --jq '.total_count'
# Census items: paginate ALL open pages (issues endpoint mixes in PRs — filter .pull_request == null)
gh api "repos/$REPO/issues?state=open&per_page=100&page=1"   # pages 1..N until empty; accumulate
# then compute: count, with_bug_label, oldest bug-labeled items
  • total open issues (note: repo metadata open_issues_count includes PRs — use the search total_count above, or your accumulated census)
  • bug-label backlog and its age: these are the "documented bugs with unmerged PRs sitting that fix them" claims. Do NOT assume a label literally named "bug" — discover the repo's bug-like label(s) first (gh api "repos/$REPO/labels?per_page=100" and pick e.g. bug, kind/bug, :bug:), then for the top 5 oldest bug-labeled issues find PRs that actually reference them. A bare search/issues?q=<number> hit is FULL-TEXT — it proves nothing about linkage; the reliable source is the issue timeline's cross-referenced events (a PR that mentions the issue shows up as source.issue with pull_request set):
    gh api -H "Accept: application/vnd.github.mockingbird-preview+json" \
      "repos/$REPO/issues/$N/timeline?per_page=100" \
      --jq '[.[] | select(.event == "cross-referenced" and .source.issue.pull_request != null) | .source.issue.number][]' \
    | while read -r n; do
        gh api "repos/$REPO/pulls/$n" --jq \
          '{number, state, merged: (.merged == true), closes_issue: ((.body // "") | test("(?i)(fix(es|ed)?|clos(es|ed)?|resolv(es|ed)?)\\s+#'"$N"'"))}'
      done
    
    A closed-but-unmerged PR with a body closing-ref is exactly the finding you want; an open one is a "sitting fix PR" — both open AND closed states matter. If the timeline returns nothing (issue closed by direct commit), fall back to search/issues?q=repo:$REPO+is:pr+<issue_number> as CANDIDATES ONLY, and verify each PR body the same way before claiming linkage. State which label(s) you used in Methodology.
  • response proxy (cheap): of the last 30 open issues, how many have a non-bot maintainer comment? (Use issues/{n}/timeline only on a small sample — expensive.)
  • stale-bot presence: does the repo run a stale-issue bot? (search issues for the stale label, or check .github/workflows). A stale bot with a short window explains low open counts — normalize for it.

Phase 5 — Governance & maintainability

  • Read .github/ contents: CONTRIBUTING.md (promised workflow), issue/PR templates, CODEOWNERS (who reviews what), FUNDING.yml, CI workflows (do they run on PRs from forks? look for the pull_request trigger).
  • Who actually merges: search result items do NOT carry merged_by — fetch it per PR from a capped sample of the 30 most recently merged items of the 3b accumulation:
    jq -r '[.[] | select(.pull_request.merged_at != null)] | sort_by(.pull_request.merged_at) | reverse | .[:30][] | .number' items.json | while read -r n; do
      gh api "repos/$REPO/pulls/$n" --jq '.merged_by.login'
    done | sort | uniq -c | sort -rn
    
    One person merging everything = single-gate risk. Bot merges (dependabot/renovate) inflate merge rates — EXCLUDE user.type == "Bot" from outside-contributor stats and note it. (Do not use pulls?state=closed pages for this — they are created-ordered.)
  • License check: OSI-approved (MIT/Apache/GPL...) vs source-available (SSPL/ELv2/BUSL/"fair use") — "open source only in name" has a license dimension. Read the LICENSE file if non-standard.
  • Openness signals: newcomer labels (good first issue, help wanted) with recent PRs by new authors; maintainer response to contribution questions; any "we don't accept PRs" language in docs/issues (quote it).

Phase 6 — Community sentiment

# HN threads (Algolia API, no auth needed). Derive the bare name from $REPO — no literal placeholders:
NAME="${REPO#*/}"   # e.g. anomalyco/opencode -> opencode
curl -s "https://hn.algolia.com/api/v1/search?query=$NAME&tags=story&hitsPerPage=10"
curl -s "https://hn.algolia.com/api/v1/search?query=$NAME&tags=comment"
  • Read the top HN thread(s); quote 1-2 comments verbatim with links — a quoted comment claiming the project is "open source in name only" is the canonical finding.
  • Reddit/Discord/Matrix: web_search for "{repo}" reddit and "{repo}" maintainers; check the project's community link in README.
  • Maintainer acknowledgment check: search issues/PRs/discussions for maintainer comments admitting a contribution bottleneck ("small team", "PRs welcome but we prioritize the roadmap"). If found, quote and link — e.g. "even their maintainers acknowledge that this is a problem".

Phase 7 — Synthesis: the verdict

SignalGOGO-IF (conditions)NO-GO
Outside merge rate> 15%5-15%< 5%
Open PR agemost < 30d30-50% older than 90dmajority older than 90d
Outside share of open PRsanymostly outsideoverwhelmingly outside with near-zero merges
First-timer outcomessome mergefew mergeclosed-without-merge pattern
Maintainer responseengages issues/PRsselectiveacknowledges bottleneck or silent
Community docsCONTRIBUTING + templates realpresent, stalemissing or contradictory

Name the verdict explicitly — this is the answer to "should I contribute?":

  • GO — outside PRs merge at healthy rates; maintainers respond; a contribution has a realistic path. Contribute.
  • GO-IF — contributing is viable only under conditions; state them (e.g. "small, well-scoped bug fixes merge; features don't" / "wait until maintainer staffing changes" / "drive-by PRs ignored but issues get linked fixes"). Contribute IF the conditions hold.
  • NO-GO — outside PRs mostly ignored, single-gate merges, ghost town, or license/governance blocks real contribution (the opencode case: "open source in name only"). Don't invest.

Then answer the requester's actual question directly ("should I contribute?" / "is this good to adopt?" / "what would my PR experience be?") with 3-6 risk/opportunity bullets, each carrying its evidence.

Report template

# Should I Contribute? — {owner}/{repo}

**Verdict: {GO | GO-IF | NO-GO}** · researched {date} · sample sizes in Methodology

## Executive summary
- …

## Snapshot
| field | value | | field | value |
|---|---|---|---|

## Momentum
commits/wk (52w), releases, bus factor …

## Contribution landscape
| metric | value |
|---|---|
| open PRs | N (M outside core) |
| open >30d / >90d | … |
| merge rate (closed in {window}, n={sample}) | … |
| outside-author merge rate | … |
| first-timer merge rate | … |
| closed-unmerged first-timers | … |

## Issue health
…

## Governance & maintainability
…

## Community sentiment
> quote (source link)

## Risks & opportunities
…

## Methodology & caveats
- endpoints used, sample sizes, research date, rate-limit constraints
- author_association split definition
- bot-exclusion rules
- known blind spots (mirror/fork contributions, private core branch, etc.)

Caveats — read before reporting

  • Unmerged ≠ rejected: PRs get superseded, closed by staleness, or merged via maintainer re-commit. Always sample the actual merged_at field; never infer from labels.
  • pulls?state=closed sorts by created_at, NOT closed_at — never call a pulls-page sample "last N closed". Use the slice-based closed: window (3b) for exact closed-at sampling.
  • Search result items report merged status at .pull_request.merged_at — top-level .merged_at is always null in search results. And never sample with sort=updated/sort=created: ordering is relevance/activity-based and empirically unusable for rates.
  • Search caps item results at 1000 per query — total_count stays exact, but per-item breakdowns need window slicing (day/week chunks ≤ 1000, full pagination, dedupe by number) per 3b.
  • Bot authors (dependabot, renovate, GitHub Actions) inflate merge rates and contributor counts — exclude user.type == "Bot" and note it.
  • Small repos: n < 30 closed PRs → say "sample too small for rate" instead of quoting a misleading percentage.
  • Rate limits: unauthenticated GitHub is 60 core req/hr + 10/min search. Budget: snapshot (3) + momentum (3+) + open-PR pages + closed-window day slices (1 req/day + one per extra 100-item page ≈ 8-10 req for 7 days, ~90+ for 90 days) + searches (2-4) ≈ 25-45 for the short-window variant. On 403, authenticate or shrink the window — never fabricate numbers.
  • Numbers decay: state the research date; merge rates move.
  • Forks and mirrors: contribution may happen in a separate repo (e.g., a "community edition") — check the README's repo links before concluding.

Workspace etiquette

  • The report is the deliverable: save it as should-i-contribute-{repo}.md when the user wants a saved artifact, otherwise print it.
  • Keep the raw JSON you computed from (optionally) — reference it in Methodology.

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

Report a problem with this skill