CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl-labs/good-oss-citizen

Rules and skills that teach AI agents how to contribute to open source projects without being the villain.

94

4.13x
Quality

93%

Does it follow best practices?

Impact

95%

4.13x

Average score across 7 eval scenarios

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

github.shskills/recon/scripts/bash/

#!/usr/bin/env bash
# GitHub API helper for good-oss-citizen tile
# Tries gh CLI first, falls back to curl for public repos
#
# Usage:
#   github.sh repo-scan <owner/repo>               - Scan repo for policy/convention files
#   github.sh issue <owner/repo> <number>           - Get issue details
#   github.sh issue-comments <owner/repo> <number>  - Get issue comments
#   github.sh check-claim <owner/repo> <number>     - Check if issue is claimed
#   github.sh issues-open <owner/repo>              - List open issues
#   github.sh issues-closed <owner/repo>            - List closed issues
#   github.sh prs-closed <owner/repo>               - List closed PRs
#   github.sh pr-history <owner/repo>               - Closed PRs with rejection reasons
#   github.sh pr-comments <owner/repo> <number>     - Get PR comments
#   github.sh file <owner/repo> <path>              - Get file contents

set -euo pipefail

COMMAND="${1:-}"
REPO="${2:-}"
ARG="${3:-}"

API="https://api.github.com"

fetch() {
    local endpoint="$1"
    if command -v gh &>/dev/null; then
        gh api "$endpoint" 2>/dev/null && return 0
    fi
    curl -sf -H "Accept: application/vnd.github+json" "${API}${endpoint}" 2>/dev/null
}

case "$COMMAND" in
    repo-scan)
        # Get default branch first, then its tree
        DEFAULT_BRANCH=$(fetch "/repos/${REPO}" | python3 -c "import sys,json; print(json.load(sys.stdin)['default_branch'])")
        BRANCH_SHA=$(fetch "/repos/${REPO}/git/refs/heads/${DEFAULT_BRANCH}" | python3 -c "import sys,json; print(json.load(sys.stdin)['object']['sha'])")
        fetch "/repos/${REPO}/git/trees/${BRANCH_SHA}?recursive=1" | python3 -c "
import sys, json

tree = json.load(sys.stdin).get('tree', [])
paths = {item['path'] for item in tree}

def check(files, label):
    found = [f for f in files if f in paths]
    missing = [f for f in files if f not in paths]
    print(f'\n=== {label} ===')
    if found: print(f'FOUND: {\", \".join(found)}')
    if missing: print(f'NOT FOUND: {\", \".join(missing)}')

check([
    'CONTRIBUTING.md', 'AI_POLICY.md', 'CODE_OF_CONDUCT.md',
    'SECURITY.md', 'DCO', 'LICENSE', 'README.md'
], 'Policy Files')

check([
    'AGENTS.md', 'CLAUDE.md', '.cursorrules',
    '.github/copilot-instructions.md', 'HOWTOAI.md', 'PROMPTING.md'
], 'Agent Instruction Files')

check([
    '.editorconfig', '.prettierrc', 'rustfmt.toml', '.clang-format',
    'pyproject.toml', '.pre-commit-config.yaml',
    'commitlint.config.js', 'commitlint.config.cjs',
    '.golangci.yml', 'Cargo.toml', 'go.mod'
], 'Convention Files')

check([
    '.github/PULL_REQUEST_TEMPLATE.md',
], 'PR Templates')

# Check for issue templates directory
issue_templates = [p for p in paths if p.startswith('.github/ISSUE_TEMPLATE')]
if issue_templates:
    print(f'\n=== Issue Templates ===')
    print(f'FOUND: {\", \".join(issue_templates)}')

# Check for test fixtures
fixtures = [p for p in paths if 'conftest.py' in p or 'test_helper' in p or 'testutil' in p]
if fixtures:
    print(f'\n=== Test Fixtures ===')
    print(f'FOUND: {\", \".join(fixtures)}')

check([
    'CHANGELOG.md', 'CODEOWNERS', 'DEVELOPMENT.md', 'Makefile',
    'justfile', 'Taskfile.yml'
], 'Build/Meta Files')

# CI workflows
workflows = [p for p in paths if p.startswith('.github/workflows/')]
if workflows:
    print(f'\n=== CI Workflows ===')
    print(f'FOUND: {\", \".join(workflows)}')
"
        ;;
    issue)
        fetch "/repos/${REPO}/issues/${ARG}" | python3 -c "
import sys, json
d = json.load(sys.stdin)
print(f\"#{d['number']}: {d['title']}\")
print(f\"State: {d['state']}\")
print(f\"Labels: {', '.join(l['name'] for l in d.get('labels', []))}\")
if d.get('assignee'): print(f\"Assigned to: {d['assignee']['login']}\")
print(f\"\n{d['body']}\")
"
        ;;
    issue-comments)
        fetch "/repos/${REPO}/issues/${ARG}/comments" | python3 -c "
import sys, json
comments = json.load(sys.stdin)
if not comments:
    print('No comments.')
else:
    for c in comments:
        print(f\"--- {c['user']['login']} ({c['created_at'][:10]}) ---\")
        print(c['body'])
        print()
"
        ;;
    check-claim)
        # DEPRECATED: Use issue-comments instead and let the LLM judge whether someone claimed the issue.
        # Regex matching can't handle claims in other languages or non-standard phrasing.
        echo "DEPRECATED: Use 'issue-comments' instead. The LLM should interpret whether any comment indicates a claim."
        echo "Running issue-comments as fallback:"
        fetch "/repos/${REPO}/issues/${ARG}/comments" | python3 -c "
import sys, json
comments = json.load(sys.stdin)
if not comments:
    print('No comments.')
else:
    for c in comments:
        print(f\"--- {c['user']['login']} ({c['created_at'][:10]}) ---\")
        print(c['body'])
        print()
"
        ;;
    issues-open)
        fetch "/repos/${REPO}/issues?state=open&per_page=30" | python3 -c "
import sys, json
issues = [i for i in json.load(sys.stdin) if 'pull_request' not in i]
for i in issues:
    labels = ', '.join(l['name'] for l in i.get('labels', []))
    assignee = i['assignee']['login'] if i.get('assignee') else 'unassigned'
    print(f\"#{i['number']}: {i['title']} [{labels}] ({assignee})\")
"
        ;;
    issues-closed)
        fetch "/repos/${REPO}/issues?state=closed&per_page=30" | python3 -c "
import sys, json
issues = [i for i in json.load(sys.stdin) if 'pull_request' not in i]
for i in issues:
    labels = ', '.join(l['name'] for l in i.get('labels', []))
    reason = i.get('state_reason', 'completed')
    print(f\"#{i['number']}: {i['title']} [{labels}] (closed: {reason})\")
"
        ;;
    prs-closed)
        fetch "/repos/${REPO}/pulls?state=closed&per_page=30" | python3 -c "
import sys, json
for p in json.load(sys.stdin):
    merged = 'merged' if p.get('merged_at') else 'closed'
    print(f\"PR #{p['number']}: {p['title']} ({merged})\")
"
        ;;
    pr-history)
        # Get closed PRs with their rejection comments — one-stop shop
        export REPO API
        fetch "/repos/${REPO}/pulls?state=closed&per_page=20" | python3 -c "
import sys, json, subprocess, os

API = os.environ.get('API', 'https://api.github.com')
REPO = os.environ.get('REPO', '')
prs = json.load(sys.stdin)

for p in prs:
    merged = 'MERGED' if p.get('merged_at') else 'CLOSED'
    print(f\"{'='*60}\")
    print(f\"PR #{p['number']}: {p['title']} ({merged})\")
    if merged == 'MERGED':
        print('  (merged successfully)')
    else:
        # Fetch comments for rejected PRs
        try:
            cmd = ['curl', '-sf', '-H', 'Accept: application/vnd.github+json',
                   f'{API}/repos/{REPO}/issues/{p[\"number\"]}/comments']
            result = subprocess.run(cmd, capture_output=True, text=True)
            comments = json.loads(result.stdout or '[]')
            if comments:
                print('  Rejection feedback:')
                for c in comments:
                    # Truncate long comments
                    body = c['body'][:500]
                    print(f\"    {c['user']['login']}: {body}\")
            else:
                print('  (no comments)')
        except:
            print('  (could not fetch comments)')
    print()
"
        ;;
    related-prs)
        # Find closed PRs related to a specific issue number
        export REPO API
        ISSUE_NUM="${ARG}"
        TMPFILE=$(mktemp)
        fetch "/repos/${REPO}/pulls?state=closed&per_page=20" > "$TMPFILE"
        TMPFILE="$TMPFILE" REPO="$REPO" API="$API" ISSUE_NUM="$ISSUE_NUM" python3 -c "
import json, subprocess, os, re

API = os.environ['API']
REPO = os.environ['REPO']
ISSUE_NUM = os.environ['ISSUE_NUM']

with open(os.environ['TMPFILE']) as f:
    prs = json.load(f)
os.unlink(os.environ['TMPFILE'])

found = []
for p in prs:
    title = p.get('title', '')
    body = p.get('body', '') or ''
    # Check if PR references this issue
    if f'#{ISSUE_NUM}' in body or f'#{ISSUE_NUM}' in title or f'issue {ISSUE_NUM}' in body.lower():
        found.append(p)

if not found:
    print(f'No closed PRs found referencing issue #{ISSUE_NUM}.')
    exit(0)

print(f'=== Closed PRs related to issue #{ISSUE_NUM} ===')
for p in found:
    num = p['number']
    merged = 'MERGED' if p.get('merged_at') else 'CLOSED'
    print(f'')
    print(f'PR #{num}: {p[\"title\"]} ({merged})')
    if merged == 'CLOSED':
        try:
            cmd = ['curl', '-sf', '-H', 'Accept: application/vnd.github+json',
                   f'{API}/repos/{REPO}/issues/{num}/comments']
            result = subprocess.run(cmd, capture_output=True, text=True)
            comments = json.loads(result.stdout or '[]')
            if comments:
                print(f'  Rejection feedback:')
                for c in comments:
                    body = c['body'][:500]
                    print(f'    {c[\"user\"][\"login\"]}: {body}')
        except:
            pass
"
        ;;
    pr-comments)
        fetch "/repos/${REPO}/issues/${ARG}/comments" | python3 -c "
import sys, json
comments = json.load(sys.stdin)
if not comments:
    print('No comments.')
else:
    for c in comments:
        print(f\"--- {c['user']['login']} ({c['created_at'][:10]}) ---\")
        print(c['body'])
        print()
"
        ;;
    file)
        fetch "/repos/${REPO}/contents/${ARG}" | python3 -c "
import sys, json, base64
d = json.load(sys.stdin)
print(base64.b64decode(d['content']).decode('utf-8'))
"
        ;;
    commit-conventions)
        # Analyze commit messages from merged PRs
        TMPFILE=$(mktemp)
        fetch "/repos/${REPO}/pulls?state=closed&per_page=10" > "$TMPFILE"
        TMPFILE="$TMPFILE" REPO="$REPO" API="$API" python3 -c "
import json, re, subprocess, os

API = os.environ['API']
REPO = os.environ['REPO']

with open(os.environ['TMPFILE']) as f:
    prs = json.load(f)
os.unlink(os.environ['TMPFILE'])

merged = [p for p in prs if p.get('merged_at')]
if not merged:
    print('No merged PRs found.')
    exit(0)

conventional = 0
signed_off = 0
messages = []

for p in merged[:5]:
    try:
        num = p['number']
        cmd = ['curl', '-sf', '-H', 'Accept: application/vnd.github+json',
               f'{API}/repos/{REPO}/pulls/{num}/commits']
        result = subprocess.run(cmd, capture_output=True, text=True)
        commits = json.loads(result.stdout or '[]')
        for commit in commits:
            msg = commit.get('commit', {}).get('message', '')
            if msg:
                first_line = msg.split(chr(10))[0]
                if first_line.startswith('Merge '):
                    continue
                messages.append(first_line)
                if re.match(r'^(feat|fix|docs|chore|refactor|test|style|perf|ci|build|revert)(\(.+\))?:', first_line):
                    conventional += 1
                if 'Signed-off-by:' in msg:
                    signed_off += 1
    except:
        pass

total = len(messages)
if total == 0:
    print('No commit messages found.')
    exit(0)

print('=== Commit Convention Analysis ===')
if conventional > total / 2:
    print(f'Format: Conventional Commits ({conventional}/{total} commits)')
else:
    print(f'Format: No strong Conventional Commits pattern ({conventional}/{total})')
if signed_off > 0:
    print(f'Signed-off-by: REQUIRED ({signed_off}/{total} commits have it)')
else:
    print(f'Signed-off-by: not detected')
print(f'')
print(f'Examples from merged PRs:')
for m in messages[:5]:
    print(f'  {m}')
"
        ;;
    branch-conventions)
        # Analyze branch names from merged PRs
        fetch "/repos/${REPO}/pulls?state=closed&per_page=10" | python3 -c '
import sys, json, re

prs = json.load(sys.stdin)
merged = [p for p in prs if p.get("merged_at")]

if not merged:
    print("No merged PRs found.")
    sys.exit(0)

branches = [p["head"]["ref"] for p in merged]
patterns = {"feat/": 0, "fix/": 0, "docs/": 0, "chore/": 0, "other": 0}
numbered = 0

print("=== Branch Naming Analysis ===")
for b in branches:
    matched = False
    for prefix in ["feat/", "fix/", "docs/", "chore/", "refactor/", "test/"]:
        if b.startswith(prefix):
            patterns[prefix] = patterns.get(prefix, 0) + 1
            matched = True
            break
    if not matched:
        patterns["other"] += 1
    if re.search(r"/\d+-", b) or re.search(r"/#?\d+", b):
        numbered += 1

dominant = max(patterns, key=patterns.get)
if patterns[dominant] > len(branches) / 2 and dominant != "other":
    print(f"Pattern: <type>/<description> (e.g., {dominant}<description>)")
    if numbered > len(branches) / 2:
        print(f"Issue numbers: YES — include issue number (e.g., fix/123-description)")
else:
    print("Pattern: No strong convention detected")

print(f"\nExamples from merged PRs:")
for b in branches[:5]:
    print(f"  {b}")
'
        ;;
    ai-policy)
        # Fetch all policy-relevant files and return their contents for LLM interpretation.
        # The LLM determines the AI stance (banned, disclosure required, conditional, no policy)
        # because policies are written in free-form prose that regex can't reliably parse.
        DEFAULT_BRANCH=$(fetch "/repos/${REPO}" | python3 -c "import sys,json; print(json.load(sys.stdin)['default_branch'])")

        echo "=== AI Policy Files ==="

        for PFILE in AI_POLICY.md CODE_OF_CONDUCT.md CONTRIBUTING.md; do
            CONTENT=$(fetch "/repos/${REPO}/contents/${PFILE}?ref=${DEFAULT_BRANCH}" 2>/dev/null | python3 -c "
import sys, json, base64
try:
    d = json.load(sys.stdin)
    print(base64.b64decode(d['content']).decode('utf-8'))
except:
    pass
" 2>/dev/null || echo "")

            if [ -n "$CONTENT" ]; then
                echo ""
                echo "--- ${PFILE} ---"
                echo "$CONTENT"
            else
                echo ""
                echo "--- ${PFILE}: NOT FOUND ---"
            fi
        done

        echo ""
        echo "=== Determine AI stance from the above files ==="
        echo "Read the files above and determine: is AI banned, is disclosure required,"
        echo "are there conditions or restrictions (e.g., good-first-issue labels),"
        echo "or is there no AI policy at all? Policies may be in any language or phrasing."
        ;;
    disclosure-format)
        # Extract the disclosure template from AI_POLICY.md
        DEFAULT_BRANCH=$(fetch "/repos/${REPO}" | python3 -c "import sys,json; print(json.load(sys.stdin)['default_branch'])")
        fetch "/repos/${REPO}/contents/AI_POLICY.md?ref=${DEFAULT_BRANCH}" 2>/dev/null | python3 -c '
import sys, json, base64, re

try:
    d = json.load(sys.stdin)
    content = base64.b64decode(d["content"]).decode("utf-8")
except:
    print("No AI_POLICY.md found — no disclosure format required.")
    sys.exit(0)

# Look for code blocks that look like templates
blocks = re.findall(r"```[\s\S]*?```", content)
template_block = None
for block in blocks:
    if "Tool:" in block or "Used for:" in block or "AI Assistance" in block:
        template_block = block.strip("`").strip()
        break

if template_block:
    print("=== Disclosure Format (copy-paste ready) ===")
    print(template_block)
else:
    # Look for bullet-style format
    lines = content.split("\n")
    in_format = False
    format_lines = []
    for line in lines:
        if "format" in line.lower() or "disclos" in line.lower() or "include" in line.lower():
            in_format = True
        if in_format:
            format_lines.append(line)
            if len(format_lines) > 10:
                break
    if format_lines:
        print("=== Disclosure Format ===")
        print("\n".join(format_lines))
    else:
        print("AI_POLICY.md exists but no specific disclosure template found.")
        print("Recommend voluntary disclosure: Tool, what it was used for, what was human-written.")
' 2>/dev/null || echo "Could not extract disclosure format"
        ;;
    pr-stats)
        # Compute PR size statistics from merged PRs
        TMPFILE=$(mktemp)
        fetch "/repos/${REPO}/pulls?state=closed&per_page=10" > "$TMPFILE"
        TMPFILE="$TMPFILE" REPO="$REPO" API="$API" python3 -c "
import json, subprocess, os

API = os.environ['API']
REPO = os.environ['REPO']

with open(os.environ['TMPFILE']) as f:
    prs = json.load(f)
os.unlink(os.environ['TMPFILE'])

merged = [p for p in prs if p.get('merged_at')]
if not merged:
    print('No merged PRs found.')
    exit(0)

additions = []
deletions = []
files = []

for p in merged[:5]:
    try:
        num = p['number']
        cmd = ['curl', '-sf', '-H', 'Accept: application/vnd.github+json',
               f'{API}/repos/{REPO}/pulls/{num}']
        result = subprocess.run(cmd, capture_output=True, text=True)
        detail = json.loads(result.stdout or '{}')
        if 'additions' in detail:
            additions.append(detail['additions'])
            deletions.append(detail['deletions'])
            files.append(detail['changed_files'])
    except:
        pass

if not additions:
    print('Could not fetch PR details.')
    exit(0)

additions.sort()
deletions.sort()
files.sort()

def median(lst):
    n = len(lst)
    if n == 0: return 0
    if n % 2 == 0: return (lst[n//2-1] + lst[n//2]) / 2
    return lst[n//2]

print('=== PR Size Statistics (from merged PRs) ===')
print(f'Sample size: {len(additions)} merged PRs')
print(f'Additions: median={int(median(additions))}, min={min(additions)}, max={max(additions)}')
print(f'Deletions: median={int(median(deletions))}, min={min(deletions)}, max={max(deletions)}')
print(f'Files changed: median={int(median(files))}, min={min(files)}, max={max(files)}')
print(f'')
print(f'Guideline: Keep your PR within ~{int(median(additions)*2)} additions and ~{int(median(files)*2)} files.')
"
        ;;
    conventions-config)
        # Read .editorconfig and .pre-commit-config.yaml, extract key settings
        DEFAULT_BRANCH=$(fetch "/repos/${REPO}" | python3 -c "import sys,json; print(json.load(sys.stdin)['default_branch'])")

        echo "=== Code Convention Config ==="

        # .editorconfig
        EC=$(fetch "/repos/${REPO}/contents/.editorconfig?ref=${DEFAULT_BRANCH}" 2>/dev/null | python3 -c "
import sys, json, base64
try:
    d = json.load(sys.stdin)
    print(base64.b64decode(d['content']).decode('utf-8'))
except:
    print('')
" 2>/dev/null || echo "")

        if [ -n "$EC" ]; then
            echo ""
            echo ".editorconfig FOUND:"
            echo "$EC"
        else
            echo ""
            echo ".editorconfig: NOT FOUND"
        fi

        # .pre-commit-config.yaml
        PC=$(fetch "/repos/${REPO}/contents/.pre-commit-config.yaml?ref=${DEFAULT_BRANCH}" 2>/dev/null | python3 -c "
import sys, json, base64
try:
    d = json.load(sys.stdin)
    print(base64.b64decode(d['content']).decode('utf-8'))
except:
    print('')
" 2>/dev/null || echo "")

        if [ -n "$PC" ]; then
            echo ""
            echo ".pre-commit-config.yaml FOUND:"
            echo "$PC"
        else
            echo ""
            echo ".pre-commit-config.yaml: NOT FOUND"
        fi

        # pyproject.toml tool sections
        PT=$(fetch "/repos/${REPO}/contents/pyproject.toml?ref=${DEFAULT_BRANCH}" 2>/dev/null | python3 -c "
import sys, json, base64
try:
    d = json.load(sys.stdin)
    content = base64.b64decode(d['content']).decode('utf-8')
    # Extract [tool.*] sections
    in_tool = False
    for line in content.split('\n'):
        if line.startswith('[tool.'):
            in_tool = True
        elif line.startswith('[') and not line.startswith('[tool.'):
            in_tool = False
        if in_tool:
            print(line)
except:
    pass
" 2>/dev/null || echo "")

        if [ -n "$PT" ]; then
            echo ""
            echo "pyproject.toml tool config:"
            echo "$PT"
        fi
        ;;
    contributing-requirements)
        # Fetch CONTRIBUTING.md and return its full contents for LLM interpretation.
        # The LLM determines requirements (DCO, changelog, tests, issue-first, etc.)
        # because contribution guidelines are free-form prose with project-specific nuance.
        DEFAULT_BRANCH=$(fetch "/repos/${REPO}" | python3 -c "import sys,json; print(json.load(sys.stdin)['default_branch'])")
        CONTENT=$(fetch "/repos/${REPO}/contents/CONTRIBUTING.md?ref=${DEFAULT_BRANCH}" 2>/dev/null | python3 -c '
import sys, json, base64
try:
    d = json.load(sys.stdin)
    print(base64.b64decode(d["content"]).decode("utf-8"))
except:
    pass
' 2>/dev/null || echo "")

        if [ -n "$CONTENT" ]; then
            echo "=== CONTRIBUTING.md ==="
            echo "$CONTENT"
            echo ""
            echo "=== Determine requirements from the above ==="
            echo "Read CONTRIBUTING.md and determine: is DCO/sign-off required or recommended?"
            echo "Are tests required? Is there an issue-first policy? Changelog updates?"
            echo "Commit format? Branch naming? Any other contribution requirements?"
        else
            echo "CONTRIBUTING.md: NOT FOUND"
        fi
        ;;
    codeowners)
        # Parse CODEOWNERS file
        DEFAULT_BRANCH=$(fetch "/repos/${REPO}" | python3 -c "import sys,json; print(json.load(sys.stdin)['default_branch'])")
        fetch "/repos/${REPO}/contents/CODEOWNERS?ref=${DEFAULT_BRANCH}" 2>/dev/null | python3 -c '
import sys, json, base64

try:
    d = json.load(sys.stdin)
    content = base64.b64decode(d["content"]).decode("utf-8")
except:
    print("CODEOWNERS: NOT FOUND")
    sys.exit(0)

print("=== Code Owners ===")
for line in content.strip().split("\n"):
    line = line.strip()
    if line and not line.startswith("#"):
        parts = line.split()
        path = parts[0]
        owners = " ".join(parts[1:])
        print(f"  {path} → {owners}")
' 2>/dev/null || echo "CODEOWNERS: NOT FOUND"
        ;;
    legal)
        # Check for DCO, CLA, license
        DEFAULT_BRANCH=$(fetch "/repos/${REPO}" | python3 -c "import sys,json; print(json.load(sys.stdin)['default_branch'])")
        BRANCH_SHA=$(fetch "/repos/${REPO}/git/refs/heads/${DEFAULT_BRANCH}" | python3 -c "import sys,json; print(json.load(sys.stdin)['object']['sha'])")

        echo "=== Legal Requirements ==="

        # Check for DCO file
        DCO=$(fetch "/repos/${REPO}/contents/DCO?ref=${DEFAULT_BRANCH}" 2>/dev/null | python3 -c "
import sys, json
try:
    json.load(sys.stdin)
    print('FOUND')
except:
    print('NOT FOUND')
" 2>/dev/null || echo "NOT FOUND")
        echo "DCO file: $DCO"

        # Check for CLA in CI
        fetch "/repos/${REPO}/git/trees/${BRANCH_SHA}?recursive=1" 2>/dev/null | python3 -c "
import sys, json
tree = json.load(sys.stdin).get('tree', [])
workflows = [f['path'] for f in tree if f['path'].startswith('.github/workflows/')]
print('CI workflows: ' + ', '.join(workflows) if workflows else 'CI workflows: none')
" 2>/dev/null

        # Check for Signed-off-by in recent commits
        fetch "/repos/${REPO}/commits?per_page=5" 2>/dev/null | python3 -c "
import sys, json
commits = json.load(sys.stdin)
signed = sum(1 for c in commits if 'Signed-off-by:' in c.get('commit', {}).get('message', ''))
total = len(commits)
if signed > 0:
    print(f'Signed-off-by in commits: YES ({signed}/{total} recent commits)')
else:
    print('Signed-off-by in commits: not detected')
" 2>/dev/null

        # License
        fetch "/repos/${REPO}/license" 2>/dev/null | python3 -c "
import sys, json
try:
    d = json.load(sys.stdin)
    lic = d.get('license', {})
    print(f\"License: {lic.get('spdx_id', 'unknown')} ({lic.get('name', 'unknown')})\")
except:
    print('License: not detected')
" 2>/dev/null
        ;;
    *)
        echo "GitHub API helper for good-oss-citizen tile"
        echo ""
        echo "Usage: $0 <command> <owner/repo> [args]"
        echo ""
        echo "Commands:"
        echo "  repo-scan <owner/repo>               Scan repo for policy/convention files"
        echo "  issue <owner/repo> <number>           Get issue details + labels + assignee"
        echo "  issue-comments <owner/repo> <number>  Get all comments on an issue"
        echo "  check-claim <owner/repo> <number>     DEPRECATED: use issue-comments instead"
        echo "  issues-open <owner/repo>              List open issues with labels"
        echo "  issues-closed <owner/repo>            List closed issues with reasons"
        echo "  prs-closed <owner/repo>               List closed/merged PRs"
        echo "  pr-history <owner/repo>               Closed PRs with rejection feedback"
        echo "  pr-comments <owner/repo> <number>     Get comments on a PR"
        echo "  pr-stats <owner/repo>                 PR size statistics from merged PRs"
        echo "  commit-conventions <owner/repo>       Detect commit message format"
        echo "  branch-conventions <owner/repo>       Detect branch naming pattern"
        echo "  ai-policy <owner/repo>                Fetch AI policy files for LLM interpretation"
        echo "  disclosure-format <owner/repo>        Extract AI disclosure template"
        echo "  conventions-config <owner/repo>        Read .editorconfig, .pre-commit-config, pyproject.toml tool sections"
        echo "  contributing-requirements <owner/repo> Fetch CONTRIBUTING.md for LLM interpretation"
        echo "  codeowners <owner/repo>                Parse CODEOWNERS file"
        echo "  legal <owner/repo>                     Check DCO, CLA, license, sign-off patterns"
        echo "  file <owner/repo> <path>               Get file contents"
        exit 1
        ;;
esac

skills

recon

README.md

tile.json