by Black Box Research Labs
You are an LLM coding agent (Claude Code, Cursor, Copilot, ChatGPT-with-tools, or similar). The human pointed you at their repository and said "do this." This file is your complete instruction set. Execute it against the current repository and produce the scorecard at the end. You need nothing else — no API keys, no installs, no extra context.
You are running a read-only governance audit of the GitHub repository this agent is currently working in. You will answer 5 questions about how the project actually reviews its own code, using only:
- the
ghCLI (already authenticated in this environment — verify withgh auth status) git(already present)jq(used only for the merge/aggregation steps that operate on already-projected fields; ships with mostghenvironments). All extraction oflogin/type/authorAssociation/stateis done withgh's built-in--jq(gojq), never with agh api … | jqpipe — see the⚠️ box below.- optionally a shallow clone for two deep-mode metrics
Hard rules — do not violate these:
- READ-ONLY. You will never write, push, comment, label, open, close, or modify anything in the user's repo or on GitHub. Every command below is a read. If you are about to run anything that mutates state, stop.
- SHOW YOUR WORK. Every number you report must be printed together with the exact command that produced it and the raw counts. The audit must be re-runnable by hand from your output alone.
- STAMP EVERY NUMBER. Never write "currently." Every figure is reported as:
<value>, last 100 merged PRs, as of <YYYY-MM-DD>plus the counting rule used. Get today's date withdate -u +%Y-%m-%dand reuse it on every line. - DETERMINISTIC METHOD. Use the exact classification rules and thresholds in §2 and the exact commands in §1/§3. Any agent running this file must get the same answer on the same repo at the same time. Do not improvise definitions; where a definition is a judgment call (Q2, Q4) state it explicitly and flag it.
- DON'T GUESS. If a command returns nothing or errors, report the empty/error result honestly. Do not fabricate counts, reviewers, or PR numbers. If you cannot compute a metric, say "could not compute" and why.
These bit earlier runs; the commands below are written to avoid them. Do not "simplify" them back:
authorAssociationis NOT agh pr list --jsonfield.gh pr list --json ...,authorAssociationfails withUnknown JSON field: "authorAssociation". It exists only at the REST top level (gh api repos/<r>/pulls/<n>→.author_association). That is why §1 enriches viagh api, not viagh pr list.gh pr list --json reviewslies about bots — bot detection on its output is UNSAFE. It strips the[bot]suffix from review-author logins (coderabbitai[bot]→coderabbitai,cubic-dev-ai[bot]→cubic-dev-ai) and omitsuser.type(author.is_botis alwaysnull). Run verbatim, every bot reviewer is mis-counted as a human and Q1/Q5 report the opposite of reality (0% bot share, 0% zero-review). Review identities MUST come fromgh api repos/<r>/pulls/<n>/reviews, which returns the real[bot]suffix anduser.type == "Bot". Never derive reviewer identity fromgh pr list.gh api …/reviews | jqis REJECTED by systemjq≥ 1.7. Review bodies on real repos (cubic-dev-ai,coderabbitai, …) contain literal control characters. Systemjq≥ 1.7 parses strictly and aborts the whole pipe withjq: error (at <stdin>:N): control characters must be escaped— so agh api repos/<r>/pulls/<n>/reviews | jq '…'pipe fails outright, often silently swallowing the PR's reviews. The fix — used everywhere below — is to do all review/association extraction withgh api … --jq '…'(gh's bundled gojq, which tolerates raw control chars). There is nogh api …/reviews | jqpipe anywhere in this file. Do not introduce one. Externaljqonly ever runs on the merge step, and only ever sees already-projected fields (number,login,type,state,submitted_at,authorAssociation) — never a raw reviewbody.
# Confirm auth + resolve the repo this agent is working in.
gh auth status
# Audit ANY repo by exporting OWNER_REPO=owner/name first; otherwise resolve
# the repo this agent is sitting in (a clone of the target).
OWNER_REPO="${OWNER_REPO:-$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null)}"
[ -n "$OWNER_REPO" ] || { echo "Set OWNER_REPO=owner/name (or run from inside a clone of the target repo)." >&2; exit 1; }
TODAY=$(date -u +%Y-%m-%d)
# Namespace ALL working files per repo + process so two concurrent runs (or two repos)
# never collide on shared /tmp paths. Everything below writes inside $WORKDIR.
WORKDIR="/tmp/wrr_$(echo "$OWNER_REPO" | tr / _)_$$"
mkdir -p "$WORKDIR"
echo "Auditing: $OWNER_REPO as of $TODAY (workdir: $WORKDIR)"Window. The core audit uses the last 100 merged PRs. This keeps the run to a couple of minutes and respects rate limits. (Alternative window if the repo is low-volume or you want a time bound: add --search "merged:>=$SINCE" to the gh pr list below, where SINCE is 180 days ago — see the cross-platform SINCE helper in §4 — for a trailing-180-day window, and say so in your stamp. State which window you used.)
Build the working set once and reuse it for Q1, Q2, Q4, Q5. This is a two-stage pull:
- Stage 1 —
gh pr listgives clean, correctly-escaped PR metadata (numbers, dates, title, body, labels). It does not carryauthorAssociationor trustworthy review-bot info (see the⚠️ box above). - Stage 2 — for each PR,
gh apiadds the two fieldsgh pr listcannot:author_associationand reviews with the real[bot]suffix +user.type. Both Stage-2 reads usegh api … --jq '…'(gojq), so raw review bodies with control characters never reach externaljq.
# Stage 1: clean PR metadata (NO authorAssociation — that field is invalid here).
gh pr list --repo "$OWNER_REPO" --state merged --limit 100 \
--json number,createdAt,mergedAt,title,labels,body \
> "$WORKDIR/base.json"
echo "Stage 1: got $(jq 'length' "$WORKDIR/base.json") merged PRs (may be <100 on young repos)"
# Stage 2: enrich each PR with author_association + REAL review identities.
# Two cheap reads per PR; ~200 calls for a full 100-PR window, well within rate limits.
# Output is newline-delimited JSON so the merge step never re-parses a whole array.
#
# CRITICAL — extraction uses gh's BUILT-IN `--jq` (gojq), NOT a `gh api ... | jq` pipe:
# * gojq tolerates the literal control characters that real review BODIES contain;
# system jq >= 1.7 would abort with "control characters must be escaped".
# * We project ONLY login + type per review (body is dropped at the gojq stage and
# NEVER touches external jq downstream).
# * Do NOT add --paginate to the per-PR /reviews call: it concatenates multiple JSON
# arrays into one stream that the projection can't parse. Per-PR review counts are
# small, so the plain single-page call below is correct and complete.
: > "$WORKDIR/enrich.jsonl"
TOTAL=$(jq 'length' "$WORKDIR/base.json"); i=0
for n in $(jq -r '.[].number' "$WORKDIR/base.json"); do
i=$((i+1)); printf '\r enriching %d/%d (PR #%s) ' "$i" "$TOTAL" "$n" >&2
# author_association via gh's --jq (gojq); emits one compact object:
gh api "repos/$OWNER_REPO/pulls/$n" \
--jq '{number, authorAssociation:.author_association}' 2>/dev/null \
>> "$WORKDIR/enrich.jsonl"
# reviews via gh's --jq (gojq) — projects login+type only, body never escapes gojq.
# Plain single-page call (NO --paginate). $n is shell-substituted into the gojq filter.
gh api "repos/$OWNER_REPO/pulls/$n/reviews" \
--jq "{number:$n, apiReviews:[.[]|{login:.user.login, type:.user.type}]}" 2>/dev/null \
>> "$WORKDIR/enrich.jsonl"
done; echo >&2
# Merge stage 1 + stage 2 into the single working set every question reads from.
# External jq here only ever sees already-projected fields (number/login/type/assoc) —
# never a raw review body — so it is safe under strict jq >= 1.7.
jq -s 'group_by(.number) | map(reduce .[] as $o ({}; . + $o)) | INDEX(.number|tostring)' \
"$WORKDIR/enrich.jsonl" > "$WORKDIR/enr_map.json"
jq --slurpfile enr "$WORKDIR/enr_map.json" \
'($enr[0]) as $e | [ .[] | . + ($e[(.number|tostring)] // {}) ]' \
"$WORKDIR/base.json" > "$WORKDIR/merged.json"
echo "Working set: $(jq 'length' "$WORKDIR/merged.json") PRs in $WORKDIR/merged.json"
# SANITY CHECK — every PR number in the working set must belong to THIS repo.
# Catches a stale/foreign $WORKDIR file or a cross-repo collision. Abort if any leak.
BASE_NUMS=$(jq -S '[.[].number]|sort' "$WORKDIR/base.json")
MERGED_NUMS=$(jq -S '[.[].number]|sort' "$WORKDIR/merged.json")
if [ "$BASE_NUMS" != "$MERGED_NUMS" ]; then
echo "FATAL: working-set PR numbers do not match Stage-1 set for $OWNER_REPO." >&2
echo " A foreign or stale file is in $WORKDIR — abort and re-run with a clean WORKDIR." >&2
exit 1
fi
# You should see real [bot] suffixes and User/Bot types here:
jq -c '.[0:3][] | {n:.number, assoc:.authorAssociation, reviewers:[.apiReviews[]?|{login,type}]}' "$WORKDIR/merged.json"What you now have in
$WORKDIR/merged.json— one object per merged PR with:number, createdAt, mergedAt, title, body, labels, authorAssociation, andapiReviews[]where each review carrieslogin(with the real[bot]suffix) andtype("User"or"Bot"). Review bodies are deliberately absent — they were dropped at the gojq projection so no control-char-laden body ever reaches externaljq. IfapiReviewsis empty for a PR, that PR genuinely had zero reviews. All five questions read this one file ($WORKDIR/merged.json) — do not re-fetch.The Q1/Q2/Q4/Q5 filters below read the live projection produced by §1: reviews under
.apiReviews[], with per-review keys.loginand.type. If instead you are replaying these filters against a pre-captured dump of the working set, the dump may use a different review schema — commonly reviews under.reviews[], author key.author, and notypefield at all. Running the §3 filters verbatim against such a dump silently yieldstotal=N, zeroReview=N(100% zero-human-review, 0% bot share) — the exact "opposite of reality" failure the⚠️ box warns about — because.apiReviews,r.type, andr.loginare allnullon every dump record. Before replaying on a dump you did not produce with §1, remap it to the live schema first (one pass, no logic change):# Normalize a frozen dump to the live $WORKDIR/merged.json schema. # Maps .reviews[] -> .apiReviews[], .author -> .login, and (since the dump # drops type) reconstructs type from the [bot] suffix so the §2 isbot rule # still has its login signal. NO numeric logic changes — pure shape fixup. jq 'map( if ((has("apiReviews")|not) and has("reviews")) then .apiReviews = [ .reviews[]? | { login: (.author // .login), type: (if (.type // null) != null then .type elif ((.author // .login // "")|test("\\[bot\\]$";"i")) then "Bot" else "User" end) } ] else . end )' "$DUMP" > "$WORKDIR/merged.json"With this remap applied, every published number reproduces exactly. The live §1 path needs no remap — it already emits
.apiReviews/.login/.type.
These are lifted as a spec from the Black Box audit engine. Apply them exactly.
Bot account. A reviewer is a bot if it satisfies any:
- the GitHub API marks it as a bot:
type == "Bot"(this is the primary, authoritative signal — present because §1 pulls reviews fromgh api), OR - its login matches (case-insensitive) any of:
- ends in
[bot]→ regex/\[bot\]$/← catches every GitHub App reviewer (coderabbitai[bot],cubic-dev-ai[bot],ellipsis-dev[bot],cursor[bot],dependabot[bot], …) once the suffix is preserved - ends in
-bot→ regex/-bot$/ - matches
/dependabot|renovate|github-actions|codecov|snyk-bot|greenkeeper|depfu|coderabbit|cubic|ellipsis|sourcery|qodo|codium/
- ends in
Why the extra names + why
typeis primary. Earlier runs mis-classified the dominant reviewer as human two ways: (1) the data source stripped[bot], and (2) modern AI reviewers like coderabbitai, cubic-dev-ai, ellipsis-dev don't contain the literal stringbot. The §1gh apisource fixes (1) —type:"Bot"and the[bot]suffix now flow through, which alone re-classifies all of them correctly. The extra login names above are belt-and-suspenders for any reviewer that ever appears without the suffix. If you still see a high-volume reviewer classified as human whose own review bodies say "automated"/"AI"/"bot," report it explicitly as a likely mis-classification rather than trusting the rule blindly.
⚠️ Whentypeis absent, classification rests ENTIRELY on the login regex — a latent fragility. The live §1 path always suppliestype, so the authoritative signal is present. But a frozen dump (see the remap box in §1) can droptypewhile preserving the[bot]suffix; on such input thetype=="Bot"branch is dead and every classification falls to the login regex alone. That regex correctly catches[bot]-suffixed apps and the-bot$/ hardcoded-vendor logins (e.g.diffray-botvia-bot$), but it has a silent gap: any bot whose login lacks[bot]/-botand is not in the hardcoded vendor list will be counted as a human with notypefallback to catch it. If you are running withouttype(dump replay) and a high-volume reviewer's name looks tool-like but slips the regex, treat it as a suspected mis-classification and verify against the livegh api .../reviewsuser.typebefore trusting the human count.
Human reviewer. A reviewer that is NOT a bot by the rule above (engine equivalent: type === "User"). A PR is "human-reviewed" if it has ≥1 review from a human.
Security PR. A PR whose title OR body OR any label matches (case-insensitive), using word boundaries on the short tokens so rce does not match source/force and cve does not match arbitrary substrings:
/secur|vuln|xss|csrf|ssrf|exploit|advisory|inject|auth.*bypass|\bcve\b|\brce\b/i
Thresholds (RED if exceeded):
| Signal | RED when |
|---|---|
| Zero-review merge rate (Q1) | > 20% |
| Single-bot review share (Q5) | > 50% of all review events from one bot account |
| Security PR closed-unmerged (Q3) | any security PR closed without a merge (count + list) |
| Kill-zone hotspot (deep mode) | a file with complexity ≥ 15 AND churn ≥ 15 |
Q2 and Q4 have no fixed numeric threshold — they require a stated definition and are reported as findings with the judgment call flagged (see below).
For each: run the command, capture the raw counts, apply the rule, stamp the result. The bot test below is shared; it reads type first (authoritative), then the login regex.
What % of the last 100 merged PRs had zero human reviewers?
The single most-bitten bug here is counting "no reviews of any kind" as the no-human rate, so the rule is pinned, not footnoted.
zeroHumanReview(the headline + the >20% gate) = merged PRs whose human-review count is0. A PR reviewed only by bots (e.g. onlycoderabbitai[bot]) has zero human reviews and counts here. Bot reviews do not rescue a PR from this bucket.zeroHumanReview = total − humanReviewed.- Decomposition (report all three so this reconciles with any audit schema):
zeroAnyReview(no reviews at all) +botOnlyReviewed(reviews exist, none human) =zeroHumanReview. Some audit records publish the two sub-counts separately (e.g.zeroReview=21 +botOnlyReviewed=49 = 70); others publish only the union (zeroReview=71). Emitting all three reproduces both.humanReviewed= merged PRs with ≥1 human review.- Partition invariant (assert both):
humanReviewed + botOnlyReviewed + zeroAnyReview == total, andzeroHumanReview == botOnlyReviewed + zeroAnyReview. If either fails, the buckets are mis-derived.- Do NOT report
zeroAnyReviewalone as the no-human rate — that is the zero-of-any-kind under-count (it drops the bot-only PRs). The gate is always onzeroHumanReview.- Percent check:
noHumanReviewPct = 100 * zeroHumanReview / total, and must back-solve to the printedzeroHumanReview.
jq -r '
def isbot(r): (r.type == "Bot")
or ((r.login // "") | test("\\[bot\\]$|-bot$|dependabot|renovate|github-actions|codecov|snyk-bot|greenkeeper|depfu|coderabbit|cubic|ellipsis|sourcery|qodo|codium";"i"));
[ .[] | { n:.number,
totalReviews: ([.apiReviews[]?] | length),
# humanReviews counts ONLY non-bot reviewers. A bot-only-reviewed PR
# has humanReviews==0 and is therefore (correctly) a zero-HUMAN-review PR.
humanReviews: ([.apiReviews[]? | select(isbot(.)|not)] | length) } ] as $prs
| ($prs|length) as $total
| ([$prs[] | select(.humanReviews>0)] | length) as $humanReviewed
# FULL DECOMPOSITION (so this reconciles with any audit schema):
# zeroAnyReview = merged PRs with NO reviews at all (empty array)
# botOnlyReviewed = merged PRs WITH reviews but 0 human reviewers
# zeroHumanReview = humanReviews==0 == zeroAnyReview + botOnlyReviewed (THE HEADLINE + the >20% gate)
| ([$prs[] | select(.totalReviews==0)] | length) as $zeroAnyReview
| ([$prs[] | select(.totalReviews>0 and .humanReviews==0)] | length) as $botOnlyReviewed
| ([$prs[] | select(.humanReviews==0)] | length) as $zeroHumanReview
| { total: $total,
humanReviewed: $humanReviewed,
zeroAnyReview: $zeroAnyReview,
botOnlyReviewed: $botOnlyReviewed,
zeroHumanReview: $zeroHumanReview,
# invariants — BOTH must hold on a correct partition:
partitionOK: (($humanReviewed + $zeroHumanReview) == $total
and ($zeroAnyReview + $botOnlyReviewed) == $zeroHumanReview),
noHumanReviewPct: (if $total>0 then (100*$zeroHumanReview/$total) else 0 end),
rate: (if $total>0 then (100*$zeroHumanReview/$total) else 0 end) }
' "$WORKDIR/merged.json"Report: zeroHumanReview/total = rate%, with the decomposition zeroAnyReview + botOnlyReviewed. RED if rate > 20%. Also list up to 5 example zero-human-review PR numbers (select(.humanReviews==0) | .n) so the user can spot-check — and confirm partitionOK == true (i.e. humanReviewed + botOnlyReviewed + zeroAnyReview == total) before trusting the row. Bot-only-reviewed PRs (reviewed solely by e.g. coderabbitai[bot]) appear in this example list by design — they have zero human reviews.
Do PRs authored by the project's own owners/maintainers get reviewed, or do they self-merge unreviewed?
JUDGMENT CALL — state this in your output. "Founder/maintainer" has no perfect API field. Use this primary definition and flag it: A PR is "maintainer-authored" if its
authorAssociationisOWNERorMEMBER.⚠️ This definition is fragile on many real repos. On a large number of OSS projects (notablycontinuedev/continue,langchain-ai/langchain,crewAIInc/crewAI), GitHub returnsauthor_association: CONTRIBUTOReven for co-founders / core maintainers — e.g. thecontinuedev/continueCTO's own PRs come back asCONTRIBUTOR, notOWNER/MEMBER. So theOWNER|MEMBERbucket can be empty even though maintainers are merging daily. IfmaintainerPRs == 0, do NOT report "100% coverage" or "no maintainer PRs" as a clean signal — report it asINCONCLUSIVEand fall back to the top-committers cross-check below (deep mode) or togh api repos/$OWNER_REPO/collaborators/ the org-members list to identify maintainers, and say which you used.
jq -r '
def isbot(r): (r.type == "Bot")
or ((r.login // "") | test("\\[bot\\]$|-bot$|dependabot|renovate|github-actions|codecov|snyk-bot|greenkeeper|depfu|coderabbit|cubic|ellipsis|sourcery|qodo|codium";"i"));
[ .[] | select(.authorAssociation=="OWNER" or .authorAssociation=="MEMBER")
| { n:.number, assoc:.authorAssociation,
humanReviews: ([.apiReviews[]? | select(isbot(.)|not)] | length) } ] as $m
| { maintainerPRs: ($m|length),
maintainerReviewed: ([$m[]|select(.humanReviews>0)]|length),
maintainerZeroReview: ([$m[]|select(.humanReviews==0)]|length) }
| . + { coveragePct: (if .maintainerPRs>0 then (100*.maintainerReviewed/.maintainerPRs) else null end) }
' "$WORKDIR/merged.json"Report: maintainerReviewed/maintainerPRs = coveragePct% reviewed; maintainerZeroReview self-merged unreviewed. State the definition used. No fixed threshold — flag as a finding if a meaningful share of maintainer PRs merged with zero human review (the same governance gap as Q1, scoped to the people with merge rights). If maintainerPRs == 0, report INCONCLUSIVE — OWNER|MEMBER empty on this repo and apply the fallback above.
Were any security-related PRs closed without ever being merged?
This query is independent of the merged-only working set — it searches closed-unmerged PRs too. Note the word-boundary tokens (\brce\b, \bcve\b) so substrings like "source", "force", "reinforce" do not produce false security matches.
# Security-matched, CLOSED, NOT merged, last 100 closed PRs.
gh pr list --repo "$OWNER_REPO" --state closed --limit 100 \
--json number,title,labels,body,mergedAt,closedAt,url | jq -r '
def secure(p): ( (p.title // "") + " " + (p.body // "") + " "
+ ([p.labels[]?.name] | join(" ")) )
| test("secur|vuln|xss|csrf|ssrf|exploit|advisory|inject|auth.*bypass|\\bcve\\b|\\brce\\b";"i");
[ .[] | select(.mergedAt==null) | select(secure(.))
| { n:.number, title:.title, closedAt:.closedAt, url:.url } ] as $dropped
| { closedUnmergedSecurityPRs: ($dropped|length), examples: ($dropped[0:5]) }
'Report the count of security-matched PRs closed without merging, with the example PR numbers/URLs. RED if count ≥ 1.
Note this Q3 pipe (
gh pr list … | jq) is safe under strictjq—gh pr listJSON is properly escaped and carries no raw review bodies (the control-char hazard lives only on the/reviewsAPI endpoint, which this query never touches).HONEST LIMITS — state all three; this is keyword matching, NOT a vuln oracle.
- Keyword matcher, not a vuln oracle. Even with word boundaries, this matches on keywords; eyeball each hit's title before calling it a real security PR (e.g. a PR titled "improve auth flow UX" can match
auth.*bypassonly if "bypass" also appears — but a "security headers" doc PR will matchsecur). In practice most hits are false positives —injectmatches "dependency injection"/"inject a mock",authmatches routine auth-flow/login UX work, anddependabot-style automated PRs frequently trip the matcher. Report the matched titles so the user can dismiss obvious non-vulns. Do not assert "N dropped security PRs = governance failure" without reading the titles.- Closed-unmerged ≠ fix never landed. This finds security-keyword PRs that were closed unmerged. It does NOT prove "no fix EVER landed" — the fix may have re-landed under a different PR or been superseded by a maintainer commit. Verifying "the vulnerability is still live" requires CVE/advisory cross-reference this audit cannot fully automate. Report the closed-unmerged-security-PR subset as the signal, and say so. If GitHub Security Advisories are public for the repo, mention
gh api repos/$OWNER_REPO/security-advisoriesas a manual next step (may require permissions).- Raw count is window-size-sensitive. The closed-unmerged-security count scales with how many closed PRs you scanned (here, last 100 closed). Report it as a rate too —
<k> closed-unmerged security hits / <N> closed PRs scanned— so it is comparable across repos and windows, not just a bare integer.
Do outside contributors wait longer to get merged than the core team?
JUDGMENT CALL — state the bucketing. Bucket each merged PR by
authorAssociation:
- INTERNAL =
OWNERorMEMBER- EXTERNAL =
CONTRIBUTORorFIRST_TIME_CONTRIBUTORorFIRST_TIMERorNONELatency =mergedAt − createdAt, reported as the median per bucket (median, not mean, to resist outliers). Flag thatCOLLABORATORPRs are excluded as ambiguous, and that association is GitHub's snapshot at audit time, not at PR-open time.⚠️ Same fragility as Q2. Because many maintainers come back asCONTRIBUTOR(see Q2), the INTERNAL bucket is frequently empty and EXTERNAL absorbs everyone — making a clean external-vs-internal comparison impossible on those repos. IfinternalCount == 0(or either bucket has n < 5), reportINCONCLUSIVE — cannot separate cohorts via author_association on this reporather than a misleading "external is slower" claim. As a fallback, you may bucket by a maintainer login list fromgh api repos/$OWNER_REPO/collaborators; if you do, state it.
jq -r '
def hrs(p): ((p.mergedAt|fromdateiso8601) - (p.createdAt|fromdateiso8601))/3600;
def med(a): (a|sort) as $s | ($s|length) as $l
| if $l==0 then null elif ($l%2==1) then $s[($l/2|floor)]
else (($s[$l/2-1]+$s[$l/2])/2) end;
[ .[] | select(.mergedAt!=null) ] as $m
| ([ $m[] | select(.authorAssociation=="OWNER" or .authorAssociation=="MEMBER") | hrs(.) ]) as $int
| ([ $m[] | select(.authorAssociation|IN("CONTRIBUTOR","FIRST_TIME_CONTRIBUTOR","FIRST_TIMER","NONE")) | hrs(.) ]) as $ext
| { internalCount: ($int|length), internalMedianHours: (med($int)),
externalCount: ($ext|length), externalMedianHours: (med($ext)) }
' "$WORKDIR/merged.json"Report both medians (convert hours→days if large: ÷24) and the n in each bucket. State the bucketing. No fixed threshold — flag as a finding if external median is materially higher than internal (the "two-speed" governance gap). If either bucket has n < 5 (or internal is empty), say the sample is too small / cohorts inseparable and mark INCONCLUSIVE.
How much of code review is done by bots, and is one bot account dominating?
jq -r '
def isbot(r): (r.type == "Bot")
or ((r.login // "") | test("\\[bot\\]$|-bot$|dependabot|renovate|github-actions|codecov|snyk-bot|greenkeeper|depfu|coderabbit|cubic|ellipsis|sourcery|qodo|codium";"i"));
[ .[] | .apiReviews[]? ] as $rv
| ($rv|length) as $allReviews
| ([ $rv[] | select(isbot(.)) ] | length) as $botReviews
| ([ $rv[] | select(isbot(.)) | .login ] | group_by(.)
| map({login: .[0], count: length}) | sort_by(-.count)) as $byBot
| { totalReviewEvents: $allReviews,
botReviewEvents: $botReviews,
botSharePct: (if $allReviews>0 then (100*$botReviews/$allReviews) else 0 end),
topBot: ($byBot[0]),
singleBotSharePct: (if $allReviews>0 and ($byBot|length)>0 then (100*$byBot[0].count/$allReviews) else 0 end) }
' "$WORKDIR/merged.json"Report: bot-review share, and the single-bot concentration (the largest one bot's share of all review events). RED if single-bot share > 50%. Name the top bot. (If you got topBot: null AND totalReviewEvents > 0: this is a valid GREEN result — the repo simply has no bot reviewers — provided your review events came from §1's apiReviews (which carry the real .type). Sanity-check by confirming every review .type is "User". ONLY suspect a data-source bug — and re-run §1 — if you bypassed §1 and read gh pr list's stripped logins, where bots masquerade as humans.)
Run this only if a clone is feasible (disk + network OK). If not, skip it and note "deep mode skipped — clone not feasible; core audit complete." Core mode (§3) is fully valid on its own.
# Cross-platform "180 days ago" (works on macOS/BSD AND Linux/GNU date):
SINCE=$(date -u -v-180d +%Y-%m-%d 2>/dev/null || date -u -d '180 days ago' +%Y-%m-%d)
echo "deep-mode window start: $SINCE"
# Depth must cover the window. On high-velocity repos --depth 200 may be < 1 week,
# so the 180-day metrics below would see almost nothing. Use a depth that reaches $SINCE,
# falling back to a generous fixed depth, then a full clone, if the shallow clone is too short.
git clone --shallow-since="$SINCE" "https://github.com/$OWNER_REPO" "$WORKDIR/clone" 2>/dev/null \
|| git clone --depth 2000 "https://github.com/$OWNER_REPO" "$WORKDIR/clone" 2>/dev/null \
|| git clone "https://github.com/$OWNER_REPO" "$WORKDIR/clone" 2>/dev/null \
&& echo "cloned" || { echo "clone failed — skip deep mode, report core only"; }
# Confirm the clone actually reaches the window (else say so and treat deep numbers as a floor):
git -C "$WORKDIR/clone" --no-pager log -1 --format='oldest reachable commit: %ci' $(git -C "$WORKDIR/clone" rev-list --max-parents=0 HEAD 2>/dev/null | tail -1) 2>/dev/nullIs the codebase's knowledge concentrated in too few people? (engine method: git shortlog -sne)
# NOTE: shortlog needs an explicit revision (HEAD). Without it, when stdout is not a TTY
# (i.e. always, when an agent captures output) it reads from EMPTY stdin and prints nothing.
git -C "$WORKDIR/clone" --no-pager shortlog -sne --since="$SINCE" HEAD | head -20Compute: total commits in window, top-1 author share, top-3 author share. Flag if top-3 share > 70% (Knowledge-Silo threshold). Print the author table (names + commit counts) as your evidence. If the table is empty, your clone did not reach $SINCE (re-clone deeper) — do not report "no commits." Note that this is commit authorship, not per-file blame — a proxy for bus-factor, not precise per-file attribution.
Which files are both complex and changing constantly — the hotspots most likely to hide bugs?
Kill-zone threshold (engine analyze.ts): complexity ≥ 15 AND churn ≥ 15.
Churn (changes per file in the window) without extra tools:
git -C "$WORKDIR/clone" --no-pager log --since="$SINCE" --name-only --pretty=format: HEAD \
| grep -v '^$' | sort | uniq -c | sort -rn | awk '$1>=15' | head -20This gives the churn ≥ 15 candidate set. If it is empty, confirm the clone reached $SINCE (Deep-A check) before concluding "no hotspots." For complexity, if a linter/cc tool is already in the repo (e.g. radon for Python, eslint complexity rule) use it on those candidate files; otherwise report the high-churn list as "kill-zone candidates (churn ≥ 15; complexity not measured — no analyzer present)" and say complexity was not computed. Do not fabricate complexity numbers.
Print exactly this shape. Tight. One row per question. Fill <> with your real numbers, then the one-line verdict. Every value carries its stamp.
WHO REVIEWS THE REVIEWERS? — <OWNER/REPO>
Window: last 100 merged PRs (got N=<n>) | As of: <YYYY-MM-DD> | Mode: core[ + deep]
# | Question | Number (stamped) | Threshold | Verdict | Reproduce
--+-----------------------------------+-------------------------------------------+------------------+---------+----------------------------------------
1 | Zero-review merge rate | <z>/<n> = <r>% | >20% = RED | <R/G> | <Q1 jq command>
2 | Maintainer PR review coverage | <a>/<b> = <c>% reviewed (def: OWNER|MEMBER)| judgment call | <flag> | <Q2 command>
3 | Security PRs closed w/o merge | <k> closed-unmerged security PRs | >=1 = RED | <R/G> | <Q3 command>
4 | External vs internal merge latency| ext med <x>d (n=<>) / int med <y>d (n=<>) | judgment call | <flag> | <Q4 command>
5 | Bot/human review deference | bot share <p>%; top bot <login> <s>% | single-bot >50% | <R/G> | <Q5 command>
--+-----------------------------------+-------------------------------------------+------------------+---------+----------------------------------------
[deep] SILO bus-factor: top-3 = <t>% of <commits> commits (180d) | >70% = flag | <flag> | git shortlog -sne --since="$SINCE" HEAD
[deep] Kill-zone: <m> files churn>=15 (complexity <measured/skipped>) | cx>=15&churn>=15 | <flag> | git log --name-only ... HEAD | uniq -c
VERDICT: <one line — e.g. "2 RED (zero-review 31%, single-bot 64%); maintainer/latency INCONCLUSIVE (OWNER|MEMBER empty). As of <date>, last 100 merged PRs.">
Rules for the scorecard:
- Every number ends in its stamp pattern:
<value>, last 100 merged PRs, as of <date>(compress in the table, but the verdict line must carry the stamp once). - Beneath the table, paste the raw counts for each question (the jq output) so the audit is self-verifying.
- For Q2/Q4 the "Verdict" column says
FLAG,OK, orINCONCLUSIVE, never RED/GREEN, because they're judgment calls — and you must have stated your definition. UseINCONCLUSIVEwhen theOWNER|MEMBERbucket is empty. Never emit a silent100%/cleanfor an empty maintainer bucket — that isINCONCLUSIVE, not a pass. - Report raw counts as rates, not bare integers. Q3 (
<k>/<N> closed PRs scanned), the deep-mode top-3 share (% of <commits> in window), and zero-review (<z>/<n>) are all window-size-sensitive — normalize them so they compare across repos and windows. - Before printing the Q1 row, assert the partition closes:
humanReviewed + botOnlyReviewed + zeroAnyReview == total(thepartitionOKfield). The<z>you print for zero-review is the zero-HUMAN-review count (zeroAnyReview + botOnlyReviewed). IfpartitionOKisfalse, do not ship the row — the buckets are mis-derived (see §6). - If any data was missing (no clone, n<100, empty reviews, empty maintainer bucket), say so in the verdict line. Graceful degradation is honest, not a failure.
- Q3 is keyword matching, not a vuln oracle, and closed-unmerged ≠ fix-never-landed. It reports security-keyword PRs closed unmerged. Word boundaries kill the worst substring false positives (
source/force), but it is still keyword matching — and in practice most hits are false positives (inject→ "dependency injection",auth→ routine login/UX work,dependabotautomated PRs). It does not prove a vulnerability is still live: a closed-unmerged hit may have re-landed under another PR or via a maintainer commit (needs CVE/advisory cross-reference no read-only script can fully automate). Eyeball every title; report the count as a rate (<k>/<N> closed PRs scanned). - Q2 ("founder") and Q4 (external/internal) are fragile and can be
INCONCLUSIVE. Both depend onauthorAssociation, which GitHub frequently returns asCONTRIBUTOR/COLLABORATOReven for core maintainers — so theOWNER|MEMBERbucket can be empty. When it is, reportINCONCLUSIVE/FLAG— never a silent100%orclean— and fall back to a collaborator/committer list. State your definition; don't overclaim. - Raw counts (Q3 hits, deep-mode top-3 share, zero-review) are window-size-sensitive — report normalized rates, not bare integers, so figures compare across repos and windows.
- The headline is
zeroHumanReview(=totalMerged − humanReviewed); report the full decomposition. A merged PR reviewed only by bots has zero human reviews and counts inzeroHumanReview. Emit all three —zeroAnyReview + botOnlyReviewed = zeroHumanReview— so the output reconciles with audit records that publish the sub-counts separately or only the union. AsserthumanReviewed + botOnlyReviewed + zeroAnyReview == totalMerged.noHumanReviewPctmust back-solve tozeroHumanReview; never reportzeroAnyReviewalone as the no-human rate. - Bot classification depends on §1 pulling reviews from
gh api(which preserves the[bot]suffix anduser.type); deriving it fromgh pr listoutput is UNSAFE — that source strips the[bot]suffix and omitsuser.type, silently mis-classifying every bot as a human. If your bot share is 0% on an active repo, you read the wrong source. - All review/association extraction is done with
gh api … --jq(gojq); agh api …/reviews | jqpipe would be rejected by systemjq≥ 1.7 on real review bodies (control characters must be escaped). Externaljqonly ever sees already-projected fields, never a raw body. - Association and bot flags are read at audit time, which can differ from PR-open time.
- Small repos (n < ~30 merged PRs) make every rate noisy — say the sample is small.
Who Reviews the Reviewers? — a repo-governance self-audit, by Black Box Research Labs. The AIV verification protocol: https://github.com/Black-Box-Research-Labs/aiv-protocol · https://blackboxresearchlabs.com