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.

95

3.55x
Quality

91%

Does it follow best practices?

Impact

96%

3.55x

Average score across 20 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.
#
# Output contract: every command prints exactly one JSON envelope on
# stdout, of shape:
#   {"command": <name>, "ok": <bool>, "data": <object|null>,
#    "warnings": [<str>, ...], "errors": [<str>, ...]}
# See ./_envelope.py for emit/fail and the shared fetch_json client.
#
# Usage:
#   github.sh <command> <owner/repo> [arg]
#
# See the help case at the bottom for the full command list.

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export PYTHONPATH="${SCRIPT_DIR}${PYTHONPATH:+:${PYTHONPATH}}"

COMMAND="${1:-}"
REPO="${2:-}"
ARG="${3:-}"
# Exported so the python heredocs (and the excepthook in _envelope.py)
# can label any failure envelope with the right command name.
export COMMAND

case "$COMMAND" in
    repo-scan)
        REPO="$REPO" python3 <<'PYEOF'
import os
from _envelope import emit, fail, fetch_json
from _templates import ISSUE_TEMPLATE_LEGACY_PATHS, issue_template_dir_paths

REPO = os.environ["REPO"]
CMD = "repo-scan"

repo_meta = fetch_json(f"/repos/{REPO}")
if not repo_meta or "default_branch" not in repo_meta:
    fail(CMD, f"could not fetch repo metadata for {REPO}")

default_branch = repo_meta["default_branch"]
ref = fetch_json(f"/repos/{REPO}/git/refs/heads/{default_branch}")
if not ref or "object" not in ref:
    fail(CMD, f"could not resolve default branch {default_branch} for {REPO}")

sha = ref["object"]["sha"]
tree = fetch_json(f"/repos/{REPO}/git/trees/{sha}?recursive=1")
if not tree or "tree" not in tree:
    fail(CMD, f"could not fetch tree {sha} for {REPO}")

paths = {item["path"] for item in tree.get("tree", []) if item.get("type") == "blob"}

def categorize(targets):
    return {
        "found": [t for t in targets if t in paths],
        "missing": [t for t in targets if t not in paths],
    }

policy_files = categorize([
    "CONTRIBUTING.md", "AI_POLICY.md", "CODE_OF_CONDUCT.md",
    "SECURITY.md", "DCO", "LICENSE", "README.md",
])
agent_instructions = categorize([
    "AGENTS.md", "CLAUDE.md", ".cursorrules",
    ".github/copilot-instructions.md", "HOWTOAI.md", "PROMPTING.md",
])
conventions = categorize([
    ".editorconfig", ".prettierrc", "rustfmt.toml", ".clang-format",
    "pyproject.toml", ".pre-commit-config.yaml",
    "commitlint.config.js", "commitlint.config.cjs",
    ".golangci.yml", "Cargo.toml", "go.mod",
])
build_meta = categorize([
    "CHANGELOG.md", "CODEOWNERS", "DEVELOPMENT.md", "Makefile",
    "justfile", "Taskfile.yml",
])

pr_template_singles = [
    ".github/PULL_REQUEST_TEMPLATE.md", ".github/pull_request_template.md",
    "docs/PULL_REQUEST_TEMPLATE.md", "PULL_REQUEST_TEMPLATE.md",
]
pr_dir = sorted(p for p in paths if p.startswith(".github/PULL_REQUEST_TEMPLATE/"))
pr_templates_found = [p for p in pr_template_singles if p in paths] + pr_dir

# `.github/ISSUE_TEMPLATE/` is the directory layout (multi-template);
# `.github/ISSUE_TEMPLATE.md` is the single-file legacy form. Match the
# directory with a trailing slash so the legacy single file isn't
# double-counted.
issue_dir = issue_template_dir_paths(paths)
issue_legacy = [p for p in ISSUE_TEMPLATE_LEGACY_PATHS if p in paths]
issue_templates_found = issue_dir + issue_legacy

emit(CMD, {
    "default_branch": default_branch,
    "policy_files": policy_files,
    "agent_instructions": agent_instructions,
    "conventions": conventions,
    "build_meta": build_meta,
    "pr_templates": {"found": pr_templates_found},
    "issue_templates": {"found": issue_templates_found},
    "test_fixtures": {
        "found": sorted(
            p for p in paths
            if "conftest.py" in p or "test_helper" in p or "testutil" in p
        )
    },
    "ci_workflows": {
        "found": sorted(p for p in paths if p.startswith(".github/workflows/"))
    },
})
PYEOF
        ;;

    issue)
        REPO="$REPO" ARG="$ARG" python3 <<'PYEOF'
import os
from _envelope import emit, fail, fetch_json

REPO = os.environ["REPO"]
ARG = os.environ["ARG"]
d = fetch_json(f"/repos/{REPO}/issues/{ARG}")
if not d or "number" not in d:
    fail("issue", f"could not fetch issue {ARG} from {REPO}")

emit("issue", {
    "number": d["number"],
    "title": d.get("title", ""),
    "state": d.get("state", ""),
    "labels": [l["name"] for l in d.get("labels", [])],
    "assignee": d["assignee"]["login"] if d.get("assignee") else None,
    "body": d.get("body") or "",
})
PYEOF
        ;;

    body)
        REPO="$REPO" ARG="$ARG" python3 <<'PYEOF'
import os
from _envelope import emit, fail, fetch_json

REPO = os.environ["REPO"]
ARG = os.environ["ARG"]
d = fetch_json(f"/repos/{REPO}/issues/{ARG}")
if not d or "number" not in d:
    fail("body", f"could not fetch issue or pull request {ARG} from {REPO}")

kind = "pull_request" if d.get("pull_request") else "issue"
emit("body", {
    "kind": kind,
    "number": d["number"],
    "title": d.get("title", ""),
    "state": d.get("state", ""),
    "url": d.get("html_url", ""),
    "body": d.get("body") or "",
})
PYEOF
        ;;

    issue-comments|pr-comments)
        REPO="$REPO" ARG="$ARG" CMD="$COMMAND" python3 <<'PYEOF'
import os
from _envelope import emit, fail, fetch_json

REPO = os.environ["REPO"]
ARG = os.environ["ARG"]
CMD = os.environ["CMD"]
comments = fetch_json(f"/repos/{REPO}/issues/{ARG}/comments")
if comments is None:
    fail(CMD, f"could not fetch comments for {ARG} on {REPO}")

emit(CMD, {
    "comments": [
        {
            "user": c.get("user", {}).get("login", ""),
            "created_at": c.get("created_at", ""),
            "body": c.get("body", ""),
        }
        for c in comments
    ]
})
PYEOF
        ;;

    check-claim)
        REPO="$REPO" ARG="$ARG" python3 <<'PYEOF'
import os
from _envelope import emit, fail, fetch_json

REPO = os.environ["REPO"]
ARG = os.environ["ARG"]
comments = fetch_json(f"/repos/{REPO}/issues/{ARG}/comments")
if comments is None:
    fail("check-claim", f"could not fetch comments for {ARG} on {REPO}")

emit("check-claim", {
    "comments": [
        {
            "user": c.get("user", {}).get("login", ""),
            "created_at": c.get("created_at", ""),
            "body": c.get("body", ""),
        }
        for c in comments
    ]
}, warnings=[
    "DEPRECATED: use 'issue-comments' instead. The LLM should interpret whether any comment indicates a claim."
])
PYEOF
        ;;

    issues-open|issues-closed)
        REPO="$REPO" CMD="$COMMAND" python3 <<'PYEOF'
import os
from _envelope import emit, fail, fetch_json

REPO = os.environ["REPO"]
CMD = os.environ["CMD"]
state = "open" if CMD == "issues-open" else "closed"
issues = fetch_json(f"/repos/{REPO}/issues?state={state}&per_page=30")
if issues is None:
    fail(CMD, f"could not fetch {state} issues for {REPO}")

filtered = [i for i in issues if "pull_request" not in i]
emit(CMD, {
    "issues": [
        {
            "number": i["number"],
            "title": i.get("title", ""),
            "state": i.get("state", state),
            "labels": [l["name"] for l in i.get("labels", [])],
            "assignee": i["assignee"]["login"] if i.get("assignee") else None,
            "state_reason": i.get("state_reason"),
        }
        for i in filtered
    ]
})
PYEOF
        ;;

    prs-closed)
        REPO="$REPO" python3 <<'PYEOF'
import os
from _envelope import emit, fail, fetch_json

REPO = os.environ["REPO"]
prs = fetch_json(f"/repos/{REPO}/pulls?state=closed&per_page=30")
if prs is None:
    fail("prs-closed", f"could not fetch closed PRs for {REPO}")

emit("prs-closed", {
    "prs": [
        {
            "number": p["number"],
            "title": p.get("title", ""),
            "merged": bool(p.get("merged_at")),
        }
        for p in prs
    ]
})
PYEOF
        ;;

    pr-history)
        REPO="$REPO" python3 <<'PYEOF'
import os
from _envelope import emit, fail, fetch_json

REPO = os.environ["REPO"]
prs = fetch_json(f"/repos/{REPO}/pulls?state=closed&per_page=20")
if prs is None:
    fail("pr-history", f"could not fetch closed PRs for {REPO}")

out = []
warnings = []
for p in prs:
    merged = bool(p.get("merged_at"))
    entry = {
        "number": p["number"],
        "title": p.get("title", ""),
        "merged": merged,
        "comments": [],
        "comments_fetch_failed": False,
    }
    if not merged:
        comments = fetch_json(f"/repos/{REPO}/issues/{p['number']}/comments")
        if comments is None:
            entry["comments_fetch_failed"] = True
            warnings.append(
                f"could not fetch comments for PR #{p['number']} — "
                "this may hide rejection feedback"
            )
        else:
            entry["comments"] = [
                {
                    "user": c.get("user", {}).get("login", ""),
                    "body": (c.get("body", "") or "")[:500],
                }
                for c in comments
            ]
    out.append(entry)

emit("pr-history", {"prs": out}, warnings=warnings)
PYEOF
        ;;

    related-prs)
        REPO="$REPO" ARG="$ARG" python3 <<'PYEOF'
import os
from _envelope import emit, fail, fetch_json

REPO = os.environ["REPO"]
ISSUE_NUM = os.environ["ARG"]
prs = fetch_json(f"/repos/{REPO}/pulls?state=closed&per_page=20")
if prs is None:
    fail("related-prs", f"could not fetch closed PRs for {REPO}")

found = []
warnings = []
for p in prs:
    title = p.get("title") or ""
    body = p.get("body") or ""
    if (f"#{ISSUE_NUM}" in body
            or f"#{ISSUE_NUM}" in title
            or f"issue {ISSUE_NUM}" in body.lower()):
        entry = {
            "number": p["number"],
            "title": p.get("title", ""),
            "merged": bool(p.get("merged_at")),
            "comments": [],
            "comments_fetch_failed": False,
        }
        if not entry["merged"]:
            comments = fetch_json(f"/repos/{REPO}/issues/{p['number']}/comments")
            if comments is None:
                entry["comments_fetch_failed"] = True
                warnings.append(
                    f"could not fetch comments for PR #{p['number']} — "
                    "this may hide rejection feedback"
                )
            else:
                entry["comments"] = [
                    {
                        "user": c.get("user", {}).get("login", ""),
                        "body": (c.get("body", "") or "")[:500],
                    }
                    for c in comments
                ]
        found.append(entry)

emit("related-prs", {"issue_number": ISSUE_NUM, "prs": found}, warnings=warnings)
PYEOF
        ;;

    file)
        REPO="$REPO" ARG="$ARG" python3 <<'PYEOF'
import base64
import os
from _envelope import emit, fail, fetch_json

REPO = os.environ["REPO"]
PATH = os.environ["ARG"]
d = fetch_json(f"/repos/{REPO}/contents/{PATH}")
if not d or "content" not in d:
    fail("file", f"could not fetch {PATH} from {REPO}")

try:
    content = base64.b64decode(d["content"]).decode("utf-8")
except (ValueError, UnicodeDecodeError) as e:
    fail("file", f"could not decode {PATH}: {e}")

emit("file", {"path": PATH, "content": content})
PYEOF
        ;;

    commit-conventions)
        REPO="$REPO" python3 <<'PYEOF'
import os
import re
from _envelope import emit, fail, fetch_json

REPO = os.environ["REPO"]
prs = fetch_json(f"/repos/{REPO}/pulls?state=closed&per_page=10")
if prs is None:
    fail("commit-conventions", f"could not fetch closed PRs for {REPO}")

merged = [p for p in prs if p.get("merged_at")]
if not merged:
    emit("commit-conventions", {
        "sample_size": 0, "conventional": 0, "signed_off": 0,
        "format": None, "signed_off_required": False, "examples": [],
    }, warnings=["no merged PRs found in the most recent 10 closed PRs"])
    raise SystemExit(0)

CONVENTIONAL = re.compile(
    r"^(feat|fix|docs|chore|refactor|test|style|perf|ci|build|revert)(\(.+\))?:"
)

messages = []
conventional = 0
signed_off = 0
warnings = []
for p in merged[:5]:
    commits = fetch_json(f"/repos/{REPO}/pulls/{p['number']}/commits")
    if commits is None:
        warnings.append(
            f"could not fetch commits for PR #{p['number']} — "
            "convention sample may under-report"
        )
        continue
    for c in commits:
        msg = c.get("commit", {}).get("message", "") or ""
        if not msg:
            continue
        first = msg.split("\n", 1)[0]
        if first.startswith("Merge "):
            continue
        messages.append(first)
        if CONVENTIONAL.match(first):
            conventional += 1
        if "Signed-off-by:" in msg:
            signed_off += 1

total = len(messages)
fmt = None
if total:
    fmt = "conventional_commits" if conventional > total / 2 else "no_strong_pattern"

emit("commit-conventions", {
    "sample_size": total,
    "conventional": conventional,
    "signed_off": signed_off,
    "format": fmt,
    "signed_off_required": signed_off > 0,
    "examples": messages[:5],
}, warnings=warnings)
PYEOF
        ;;

    branch-conventions)
        REPO="$REPO" python3 <<'PYEOF'
import os
import re
from _envelope import emit, fail, fetch_json

REPO = os.environ["REPO"]
prs = fetch_json(f"/repos/{REPO}/pulls?state=closed&per_page=10")
if prs is None:
    fail("branch-conventions", f"could not fetch closed PRs for {REPO}")

merged = [p for p in prs if p.get("merged_at")]
if not merged:
    emit("branch-conventions", {
        "sample_size": 0, "patterns": {}, "numbered": 0,
        "dominant": None, "issue_numbers_in_branch": False, "examples": [],
    }, warnings=["no merged PRs found"])
    raise SystemExit(0)

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

for b in branches:
    matched = False
    for prefix in prefixes:
        if b.startswith(prefix):
            patterns[prefix] += 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 or dominant == "other":
    dominant = None

emit("branch-conventions", {
    "sample_size": len(branches),
    "patterns": patterns,
    "numbered": numbered,
    "dominant": dominant,
    "issue_numbers_in_branch": numbered > len(branches) / 2,
    "examples": branches[:5],
})
PYEOF
        ;;

    ai-policy)
        REPO="$REPO" python3 <<'PYEOF'
import base64
import os
from _envelope import emit, fail, fetch_json

REPO = os.environ["REPO"]
repo_meta = fetch_json(f"/repos/{REPO}")
if not repo_meta or "default_branch" not in repo_meta:
    fail("ai-policy", f"could not fetch repo metadata for {REPO}")
ref = repo_meta["default_branch"]

results = []
for path in ("AI_POLICY.md", "CODE_OF_CONDUCT.md", "CONTRIBUTING.md"):
    d = fetch_json(f"/repos/{REPO}/contents/{path}?ref={ref}")
    if not d or "content" not in d:
        results.append({"path": path, "found": False, "content": None})
        continue
    try:
        content = base64.b64decode(d["content"]).decode("utf-8")
    except (ValueError, UnicodeDecodeError):
        results.append({"path": path, "found": False, "content": None})
        continue
    results.append({"path": path, "found": True, "content": content})

emit("ai-policy", {"default_branch": ref, "files": results})
PYEOF
        ;;

    disclosure-format)
        REPO="$REPO" python3 <<'PYEOF'
import base64
import os
import re
from _envelope import emit, fail, fetch_json

REPO = os.environ["REPO"]
repo_meta = fetch_json(f"/repos/{REPO}")
if not repo_meta or "default_branch" not in repo_meta:
    fail("disclosure-format", f"could not fetch repo metadata for {REPO}")
ref = repo_meta["default_branch"]

d = fetch_json(f"/repos/{REPO}/contents/AI_POLICY.md?ref={ref}")
if not d or "content" not in d:
    emit("disclosure-format", {"format": "none", "template": None},
         warnings=["AI_POLICY.md not found — no disclosure format required"])
    raise SystemExit(0)

try:
    content = base64.b64decode(d["content"]).decode("utf-8")
except (ValueError, UnicodeDecodeError):
    fail("disclosure-format", "could not decode AI_POLICY.md")

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:
    emit("disclosure-format", {"format": "code_block", "template": template_block})
    raise SystemExit(0)

# Bullet/prose fallback — collect a small window around the disclosure heading
in_format = False
format_lines = []
for line in content.split("\n"):
    low = line.lower()
    if "format" in low or "disclos" in low or "include" in low:
        in_format = True
    if in_format:
        format_lines.append(line)
        if len(format_lines) > 10:
            break

if format_lines:
    emit("disclosure-format", {"format": "prose", "template": "\n".join(format_lines)})
    raise SystemExit(0)

emit("disclosure-format", {"format": "none", "template": None},
     warnings=["AI_POLICY.md exists but no specific disclosure template found — recommend voluntary disclosure"])
PYEOF
        ;;

    pr-stats)
        REPO="$REPO" python3 <<'PYEOF'
import os
from _envelope import emit, fail, fetch_json

REPO = os.environ["REPO"]
prs = fetch_json(f"/repos/{REPO}/pulls?state=closed&per_page=10")
if prs is None:
    fail("pr-stats", f"could not fetch closed PRs for {REPO}")

merged = [p for p in prs if p.get("merged_at")]
if not merged:
    emit("pr-stats", {"sample_size": 0}, warnings=["no merged PRs found"])
    raise SystemExit(0)

additions, deletions, files = [], [], []
for p in merged[:5]:
    detail = fetch_json(f"/repos/{REPO}/pulls/{p['number']}")
    if detail and "additions" in detail:
        additions.append(detail["additions"])
        deletions.append(detail["deletions"])
        files.append(detail["changed_files"])

if not additions:
    emit("pr-stats", {"sample_size": 0}, warnings=["could not fetch any PR details"])
    raise SystemExit(0)


def stats(values):
    s = sorted(values)
    n = len(s)
    median = s[n // 2] if n % 2 else (s[n // 2 - 1] + s[n // 2]) / 2
    return {"median": median, "min": min(s), "max": max(s)}


a, dst, f = stats(additions), stats(deletions), stats(files)
emit("pr-stats", {
    "sample_size": len(additions),
    "additions": a,
    "deletions": dst,
    "files": f,
    "guideline": {
        "max_additions": int(a["median"] * 2),
        "max_files": int(f["median"] * 2),
    },
})
PYEOF
        ;;

    conventions-config)
        REPO="$REPO" python3 <<'PYEOF'
import base64
import os
from _envelope import emit, fail, fetch_json

REPO = os.environ["REPO"]
repo_meta = fetch_json(f"/repos/{REPO}")
if not repo_meta or "default_branch" not in repo_meta:
    fail("conventions-config", f"could not fetch repo metadata for {REPO}")
ref = repo_meta["default_branch"]


def get_text(path):
    d = fetch_json(f"/repos/{REPO}/contents/{path}?ref={ref}")
    if not d or "content" not in d:
        return None
    try:
        return base64.b64decode(d["content"]).decode("utf-8")
    except (ValueError, UnicodeDecodeError):
        return None


ec = get_text(".editorconfig")
pc = get_text(".pre-commit-config.yaml")
pt = get_text("pyproject.toml")
pt_tool = None
if pt:
    in_tool = False
    keep = []
    for line in pt.split("\n"):
        if line.startswith("[tool."):
            in_tool = True
        elif line.startswith("[") and not line.startswith("[tool."):
            in_tool = False
        if in_tool:
            keep.append(line)
    pt_tool = "\n".join(keep) if keep else None

emit("conventions-config", {
    "default_branch": ref,
    "editorconfig": {"found": ec is not None, "content": ec},
    "pre_commit_config": {"found": pc is not None, "content": pc},
    "pyproject_tool": {"found": pt_tool is not None, "content": pt_tool},
})
PYEOF
        ;;

    contributing-requirements)
        REPO="$REPO" python3 <<'PYEOF'
import base64
import os
from _envelope import emit, fail, fetch_json

REPO = os.environ["REPO"]
repo_meta = fetch_json(f"/repos/{REPO}")
if not repo_meta or "default_branch" not in repo_meta:
    fail("contributing-requirements", f"could not fetch repo metadata for {REPO}")
ref = repo_meta["default_branch"]

d = fetch_json(f"/repos/{REPO}/contents/CONTRIBUTING.md?ref={ref}")
if not d or "content" not in d:
    emit("contributing-requirements", {"found": False, "content": None})
    raise SystemExit(0)

try:
    content = base64.b64decode(d["content"]).decode("utf-8")
except (ValueError, UnicodeDecodeError):
    fail("contributing-requirements", "could not decode CONTRIBUTING.md")

emit("contributing-requirements", {"found": True, "content": content})
PYEOF
        ;;

    codeowners)
        REPO="$REPO" python3 <<'PYEOF'
import base64
import os
from _envelope import emit, fail, fetch_json

REPO = os.environ["REPO"]
repo_meta = fetch_json(f"/repos/{REPO}")
if not repo_meta or "default_branch" not in repo_meta:
    fail("codeowners", f"could not fetch repo metadata for {REPO}")
ref = repo_meta["default_branch"]

content = None
for candidate in ("CODEOWNERS", ".github/CODEOWNERS", "docs/CODEOWNERS"):
    d = fetch_json(f"/repos/{REPO}/contents/{candidate}?ref={ref}")
    if not d or "content" not in d:
        continue
    try:
        content = base64.b64decode(d["content"]).decode("utf-8")
        break
    except (ValueError, UnicodeDecodeError):
        continue

if content is None:
    emit("codeowners", {"found": False, "rules": []})
    raise SystemExit(0)

rules = []
for line in content.strip().split("\n"):
    line = line.strip()
    if not line or line.startswith("#"):
        continue
    parts = line.split()
    if parts:
        rules.append({"path": parts[0], "owners": parts[1:]})

emit("codeowners", {"found": True, "rules": rules})
PYEOF
        ;;

    legal)
        REPO="$REPO" python3 <<'PYEOF'
import os
from _envelope import emit, fail, fetch_json

REPO = os.environ["REPO"]
repo_meta = fetch_json(f"/repos/{REPO}")
if not repo_meta or "default_branch" not in repo_meta:
    fail("legal", f"could not fetch repo metadata for {REPO}")
ref = repo_meta["default_branch"]

ref_data = fetch_json(f"/repos/{REPO}/git/refs/heads/{ref}")
if not ref_data or "object" not in ref_data:
    fail("legal", f"could not resolve branch {ref}")
sha = ref_data["object"]["sha"]

warnings = []

# A 404 on the DCO endpoint legitimately means "no DCO file"; a
# network/auth failure also returns None. fetch_json can't distinguish
# the two, so dco_file is best-effort: False here means "we got nothing
# back at all", which encompasses both the absent-file and fetch-failed
# cases. There's no `dco_file_known` flag because there's no operation
# we could do to disambiguate without false certainty.
dco_resp = fetch_json(f"/repos/{REPO}/contents/DCO?ref={ref}")
dco_present = bool(dco_resp and "content" in dco_resp)

# Distinguish None (fetch failure) from empty results so consumers can
# trust an absent ci_workflows / signed_off_total reading.
tree = fetch_json(f"/repos/{REPO}/git/trees/{sha}?recursive=1")
if tree is None:
    workflows = []
    workflows_known = False
    warnings.append(
        f"could not fetch repository tree for {sha} — "
        "ci_workflows is incomplete"
    )
else:
    workflows = sorted(
        f["path"] for f in tree.get("tree", [])
        if f["path"].startswith(".github/workflows/")
    )
    workflows_known = True

commits = fetch_json(f"/repos/{REPO}/commits?per_page=5")
if commits is None:
    commits = []
    commits_known = False
    warnings.append(
        "could not fetch recent commits — "
        "signed_off_count / signed_off_total is incomplete"
    )
else:
    commits_known = True
signed = sum(
    1 for c in commits
    if "Signed-off-by:" in (c.get("commit", {}).get("message", "") or "")
)

license_data = fetch_json(f"/repos/{REPO}/license")
if license_data is None:
    license_info = None
    license_known = False
    warnings.append("could not fetch /license — `license` is incomplete")
else:
    lic = license_data.get("license") or {}
    license_info = (
        {"spdx_id": lic.get("spdx_id"), "name": lic.get("name")}
        if lic else None
    )
    license_known = True

emit("legal", {
    "default_branch": ref,
    "dco_file": dco_present,
    "ci_workflows": workflows,
    "ci_workflows_known": workflows_known,
    "signed_off_count": signed,
    "signed_off_total": len(commits),
    "signed_off_known": commits_known,
    "license": license_info,
    "license_known": license_known,
}, warnings=warnings)
PYEOF
        ;;

    templates-issue)
        REPO="$REPO" python3 <<'PYEOF'
import base64
import os
from _envelope import emit, fail, fetch_json
from _templates import ISSUE_TEMPLATE_LEGACY_PATHS, issue_template_dir_paths

REPO = os.environ["REPO"]
repo_meta = fetch_json(f"/repos/{REPO}")
if not repo_meta or "default_branch" not in repo_meta:
    fail("templates-issue", f"could not fetch repo metadata for {REPO}")
ref = repo_meta["default_branch"]

ref_data = fetch_json(f"/repos/{REPO}/git/refs/heads/{ref}")
if not ref_data or "object" not in ref_data:
    fail("templates-issue", f"could not resolve branch {ref}")
sha = ref_data["object"]["sha"]

# Tree fetch failure must NOT silently look like "no templates" — that
# would let a transient API failure masquerade as a clean absent answer.
tree = fetch_json(f"/repos/{REPO}/git/trees/{sha}?recursive=1")
if tree is None:
    fail("templates-issue",
         f"could not fetch repository tree for {sha} — cannot enumerate templates")
paths = [
    item["path"] for item in tree.get("tree", [])
    if item.get("type") == "blob"
]

dir_templates = issue_template_dir_paths(paths, extensions=(".md", ".yml", ".yaml"))
legacy = [p for p in ISSUE_TEMPLATE_LEGACY_PATHS if p in paths]
ordered = dir_templates if dir_templates else legacy

if not ordered:
    emit("templates-issue", {"default_branch": ref, "templates": []})
    raise SystemExit(0)

templates = []
fetch_failures = []
for path in ordered:
    d = fetch_json(f"/repos/{REPO}/contents/{path}?ref={ref}")
    if not d or "content" not in d:
        fetch_failures.append(path)
        continue
    try:
        body = base64.b64decode(d["content"]).decode("utf-8")
    except (ValueError, UnicodeDecodeError):
        fetch_failures.append(path)
        continue
    if not body.strip():
        continue  # treat empty file as absent — matches GitHub's own behavior
    templates.append({"path": path, "content": body.rstrip()})

# error-handling: don't let a fetch failure masquerade as "no templates".
# If discovery returned paths but every fetch failed, that's an upstream
# read failure, not absence — fail loudly so the consumer can't conclude
# "this repo has no issue templates".
if ordered and not templates and fetch_failures:
    fail("templates-issue",
         f"discovered {len(ordered)} template path(s) but could not fetch/decode any: "
         f"{', '.join(fetch_failures)}")

# Partial failure: some paths fetched, some didn't — keep the successes
# but warn about the missing ones so the consumer can spot incomplete
# data.
warnings = []
if fetch_failures and templates:
    warnings.append(
        f"could not fetch/decode {len(fetch_failures)} of {len(ordered)} "
        f"template path(s): {', '.join(fetch_failures)}"
    )

emit("templates-issue", {"default_branch": ref, "templates": templates}, warnings=warnings)
PYEOF
        ;;

    templates-pr)
        REPO="$REPO" python3 <<'PYEOF'
import base64
import os
from _envelope import emit, fail, fetch_json

REPO = os.environ["REPO"]
repo_meta = fetch_json(f"/repos/{REPO}")
if not repo_meta or "default_branch" not in repo_meta:
    fail("templates-pr", f"could not fetch repo metadata for {REPO}")
ref = repo_meta["default_branch"]

ref_data = fetch_json(f"/repos/{REPO}/git/refs/heads/{ref}")
if not ref_data or "object" not in ref_data:
    fail("templates-pr", f"could not resolve branch {ref}")
sha = ref_data["object"]["sha"]

tree = fetch_json(f"/repos/{REPO}/git/trees/{sha}?recursive=1")
if tree is None:
    fail("templates-pr",
         f"could not fetch repository tree for {sha} — cannot enumerate templates")
paths = [
    item["path"] for item in tree.get("tree", [])
    if item.get("type") == "blob"
]


def ci_match(path, candidate):
    return path.lower() == candidate.lower()


single_candidates = (
    ".github/PULL_REQUEST_TEMPLATE.md",
    "docs/PULL_REQUEST_TEMPLATE.md",
    "PULL_REQUEST_TEMPLATE.md",
)
single_found = []
for cand in single_candidates:
    for p in paths:
        if ci_match(p, cand):
            single_found.append(p)
            break

dir_templates = sorted(
    p for p in paths
    if p.lower().startswith(".github/pull_request_template/")
    and p.lower().endswith(".md")
)

# .github/PULL_REQUEST_TEMPLATE.md first, then directory templates,
# then docs/ and root fallbacks.
ordered = [p for p in single_found if p.lower().startswith(".github/pull_request_template.md")]
ordered.extend(dir_templates)
ordered.extend(p for p in single_found if not p.lower().startswith(".github/pull_request_template.md"))

if not ordered:
    emit("templates-pr", {"default_branch": ref, "templates": []})
    raise SystemExit(0)

templates = []
fetch_failures = []
for path in ordered:
    d = fetch_json(f"/repos/{REPO}/contents/{path}?ref={ref}")
    if not d or "content" not in d:
        fetch_failures.append(path)
        continue
    try:
        body = base64.b64decode(d["content"]).decode("utf-8")
    except (ValueError, UnicodeDecodeError):
        fetch_failures.append(path)
        continue
    if not body.strip():
        continue  # treat empty file as absent — matches GitHub's own behavior
    templates.append({"path": path, "content": body.rstrip()})

# Same semantics as templates-issue: discovered-but-unfetchable is an
# upstream read failure, not absence.
if ordered and not templates and fetch_failures:
    fail("templates-pr",
         f"discovered {len(ordered)} template path(s) but could not fetch/decode any: "
         f"{', '.join(fetch_failures)}")

warnings = []
if fetch_failures and templates:
    warnings.append(
        f"could not fetch/decode {len(fetch_failures)} of {len(ordered)} "
        f"template path(s): {', '.join(fetch_failures)}"
    )

emit("templates-pr", {"default_branch": ref, "templates": templates}, warnings=warnings)
PYEOF
        ;;

    *)
        CMD="$COMMAND" python3 <<'PYEOF'
import os
import sys
from _envelope import emit

cmd = os.environ.get("CMD", "")
available = [
    "repo-scan", "issue", "body", "issue-comments", "check-claim",
    "issues-open", "issues-closed", "prs-closed", "pr-history",
    "related-prs", "pr-comments", "file", "commit-conventions",
    "branch-conventions", "ai-policy", "disclosure-format",
    "pr-stats", "conventions-config", "contributing-requirements",
    "codeowners", "legal", "templates-issue", "templates-pr",
]

# script-delegation.md self-error-handling: stderr diagnostic on every
# failure exit, even the dispatcher's unknown-command path.
if not cmd:
    sys.stderr.write("github.sh: no command provided. Run with one of: "
                     + ", ".join(available) + "\n")
    emit("help", {"available_commands": available},
         errors=["no command provided"], ok=False)
else:
    sys.stderr.write(f"github.sh: unknown command: {cmd}. Available: "
                     + ", ".join(available) + "\n")
    emit(cmd, {"available_commands": available},
         errors=[f"unknown command: {cmd}"], ok=False)
PYEOF
        exit 1
        ;;
esac

README.md

tile.json