@intentsolutionsio/contributing-clanker 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +173 -0
- package/hooks/.gitkeep +0 -0
- package/hooks/install.sh +115 -0
- package/hooks/uninstall.sh +70 -0
- package/package.json +42 -0
- package/skills/contribute/SKILL.md +457 -0
- package/skills/contribute/agents/draft-writer.md +110 -0
- package/skills/contribute/agents/repo-analyzer.md +68 -0
- package/skills/contribute/agents/researcher.md +246 -0
- package/skills/contribute/agents/scout.md +182 -0
- package/skills/contribute/agents/test-runner.md +70 -0
- package/skills/contribute/assets/claim-template.md +22 -0
- package/skills/contribute/assets/evidence-template.md +32 -0
- package/skills/contribute/assets/pr-template.md +49 -0
- package/skills/contribute/references/candidate-file-format.md +259 -0
- package/skills/contribute/references/workflow-guide.md +153 -0
- package/skills/contribute/scripts/audit-overrides.sh +180 -0
- package/skills/contribute/scripts/catalog-coverage.sh +99 -0
- package/skills/contribute/scripts/gate-runner.sh +191 -0
- package/skills/contribute/scripts/gates/a01-already-assigned.sh +24 -0
- package/skills/contribute/scripts/gates/a02-already-shipped.sh +31 -0
- package/skills/contribute/scripts/gates/a03-duplicate-flagged.sh +22 -0
- package/skills/contribute/scripts/gates/a04-issue-age.sh +80 -0
- package/skills/contribute/scripts/gates/a05-issue-still-open.sh +33 -0
- package/skills/contribute/scripts/gates/a06-claim-etiquette-required.sh +36 -0
- package/skills/contribute/scripts/gates/a09-mention-routing.sh +63 -0
- package/skills/contribute/scripts/gates/b01-base-branch.sh +29 -0
- package/skills/contribute/scripts/gates/b02-branch-naming.sh +34 -0
- package/skills/contribute/scripts/gates/b03-clone-fresh.sh +33 -0
- package/skills/contribute/scripts/gates/b05-dco-signoff.sh +50 -0
- package/skills/contribute/scripts/gates/b06-commit-format.sh +44 -0
- package/skills/contribute/scripts/gates/b07-scope-files.sh +68 -0
- package/skills/contribute/scripts/gates/b12-new-deps.sh +40 -0
- package/skills/contribute/scripts/gates/b14-local-checks.sh +55 -0
- package/skills/contribute/scripts/gates/b16-local-check-allowlist.sh +32 -0
- package/skills/contribute/scripts/gates/c01-draft-first.sh +23 -0
- package/skills/contribute/scripts/gates/c02-pr-title-format.sh +36 -0
- package/skills/contribute/scripts/gates/c03-pr-body-sections.sh +58 -0
- package/skills/contribute/scripts/gates/c04-ui-screenshots.sh +38 -0
- package/skills/contribute/scripts/gates/c05-test-evidence.sh +31 -0
- package/skills/contribute/scripts/gates/c07-coauthor-banned.sh +30 -0
- package/skills/contribute/scripts/gates/c09-issue-link.sh +31 -0
- package/skills/contribute/scripts/gates/c11-no-force-push.sh +32 -0
- package/skills/contribute/scripts/gates/c12-ci-green.sh +29 -0
- package/skills/contribute/scripts/gates/c13-bots-passed.sh +62 -0
- package/skills/contribute/scripts/gates/c16-no-self-merge.sh +24 -0
- package/skills/contribute/scripts/gates/c19-body-claim-vs-diff.sh +64 -0
- package/skills/contribute/scripts/gates/d02-no-ai-bug-reports.sh +48 -0
- package/skills/contribute/scripts/gates/d03-no-ai-pr-reviews.sh +42 -0
- package/skills/contribute/scripts/gates/d05-no-reopen.sh +25 -0
- package/skills/contribute/scripts/gates/e02-ai-strike-track.sh +57 -0
- package/skills/contribute/scripts/gates/e04-fork-target.sh +39 -0
- package/skills/contribute/scripts/gates/f01-license-compat.sh +92 -0
- package/skills/contribute/scripts/gates/f03-fixtures-clean.sh +30 -0
- package/skills/contribute/scripts/gates/f04-override-disclosure.sh +49 -0
- package/skills/contribute/scripts/gates/g01-no-vendored-edits.sh +52 -0
- package/skills/contribute/scripts/gates/g02-protected-paths.sh +30 -0
- package/skills/contribute/scripts/gates/g03-no-changelog-edits.sh +37 -0
- package/skills/contribute/scripts/gates/g04-no-version-bump.sh +36 -0
- package/skills/contribute/scripts/gates/g06-override-rate-limit.sh +31 -0
- package/skills/contribute/scripts/gates/lib/preamble.sh +105 -0
- package/skills/contribute/scripts/lint-candidate.sh +149 -0
- package/skills/contribute/scripts/researcher-build.sh +456 -0
- package/skills/contribute/scripts/test-known-traps.sh +142 -0
- package/skills/contribute/scripts/test-override-audit.sh +102 -0
- package/skills/contribute/scripts/test-plug-in.sh +113 -0
- package/skills/contribute/scripts/test-scout-refresh.sh +157 -0
- package/skills/contribute/scripts/test-stale-dossier-refresh.sh +96 -0
- package/skills/contribute/scripts/transition.sh +260 -0
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# researcher-build.sh — build a per-repo dossier of contribution rules.
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# researcher-build.sh <owner>/<repo> [--no-link-follow] [--stdout]
|
|
6
|
+
#
|
|
7
|
+
# Default behavior: writes the dossier to
|
|
8
|
+
# ~/.contribute-system/research/<owner>__<repo>.md
|
|
9
|
+
# and prints "→ wrote dossier to <path>" to stderr so the caller has feedback.
|
|
10
|
+
#
|
|
11
|
+
# Override the output path via CONTRIBUTE_RESEARCH_DIR env var (the dossier
|
|
12
|
+
# lands at $CONTRIBUTE_RESEARCH_DIR/<owner>__<repo>.md).
|
|
13
|
+
#
|
|
14
|
+
# Pass --stdout to write the dossier to stdout instead — useful for piping
|
|
15
|
+
# (jq, less, diff against an existing dossier).
|
|
16
|
+
#
|
|
17
|
+
# What it pulls:
|
|
18
|
+
# - Repo metadata (stars, default branch, archived, push activity)
|
|
19
|
+
# - Policy file inventory (CONTRIBUTING, CLA, DCO, AI_POLICY, SECURITY,
|
|
20
|
+
# CODEOWNERS, PR template, code of conduct, governance)
|
|
21
|
+
# - Raw CONTRIBUTING.md (with key excerpts)
|
|
22
|
+
# - Depth-1 follow of links inside CONTRIBUTING.md (handbook, AI policy,
|
|
23
|
+
# review guide) — saved with fetched-at timestamps
|
|
24
|
+
# - External merge friendliness (last 90 days)
|
|
25
|
+
# - Bots that auto-review on this repo (sampled from a recent PR)
|
|
26
|
+
# - Convention detection: commit format, branch naming, sign-off, CLA, AI
|
|
27
|
+
#
|
|
28
|
+
# Design notes:
|
|
29
|
+
# - Read-only against GitHub. Never writes to upstream.
|
|
30
|
+
# - Uses temp dir for intermediate files; cleans up on exit.
|
|
31
|
+
# - All gh / curl failures degrade gracefully — partial dossier > nothing.
|
|
32
|
+
|
|
33
|
+
set -uo pipefail
|
|
34
|
+
|
|
35
|
+
REPO=""
|
|
36
|
+
NO_LINK_FOLLOW=""
|
|
37
|
+
TO_STDOUT=0
|
|
38
|
+
for arg in "$@"; do
|
|
39
|
+
case "$arg" in
|
|
40
|
+
--no-link-follow) NO_LINK_FOLLOW="--no-link-follow" ;;
|
|
41
|
+
--stdout) TO_STDOUT=1 ;;
|
|
42
|
+
-h|--help) /usr/bin/sed -n '2,16p' "$0" | /usr/bin/sed 's/^# \{0,1\}//'; exit 0 ;;
|
|
43
|
+
-*) /usr/bin/echo "unknown flag: $arg" >&2; exit 64 ;;
|
|
44
|
+
*) if [[ -z "$REPO" ]]; then REPO="$arg"; else /usr/bin/echo "extra arg: $arg" >&2; exit 64; fi ;;
|
|
45
|
+
esac
|
|
46
|
+
done
|
|
47
|
+
|
|
48
|
+
if [[ -z "$REPO" || "$REPO" != */* ]]; then
|
|
49
|
+
/usr/bin/echo "usage: $0 <owner>/<repo> [--no-link-follow] [--stdout]" >&2
|
|
50
|
+
exit 64
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
if ! gh auth status >/dev/null 2>&1; then
|
|
54
|
+
echo "gh: not authenticated" >&2
|
|
55
|
+
exit 65
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
TMPDIR=$(/usr/bin/mktemp -d)
|
|
59
|
+
trap 'rm -rf "$TMPDIR"' EXIT
|
|
60
|
+
NOW=$(/usr/bin/date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
61
|
+
NINETY_AGO=$(/usr/bin/date -u -d '90 days ago' +%s)
|
|
62
|
+
|
|
63
|
+
log() { /usr/bin/echo "researcher-build: $*" >&2; }
|
|
64
|
+
|
|
65
|
+
# ---- 1. Repo metadata ----
|
|
66
|
+
log "[1/8] fetching repo metadata for $REPO"
|
|
67
|
+
META_FILE="$TMPDIR/meta.json"
|
|
68
|
+
gh api "repos/$REPO" > "$META_FILE" 2>/dev/null || { log "ERROR: repo $REPO not accessible"; exit 66; }
|
|
69
|
+
STARS=$(jq -r '.stargazers_count // 0' < "$META_FILE")
|
|
70
|
+
DEFAULT_BRANCH=$(jq -r '.default_branch // "main"' < "$META_FILE")
|
|
71
|
+
ARCHIVED=$(jq -r '.archived // false' < "$META_FILE")
|
|
72
|
+
PUSHED_AT=$(jq -r '.pushed_at // ""' < "$META_FILE")
|
|
73
|
+
LICENSE=$(jq -r '.license.spdx_id // "UNKNOWN"' < "$META_FILE")
|
|
74
|
+
LANG=$(jq -r '.language // "unknown"' < "$META_FILE")
|
|
75
|
+
DESC=$(jq -r '.description // ""' < "$META_FILE" | /usr/bin/head -c 200)
|
|
76
|
+
|
|
77
|
+
# ---- 2. Policy file inventory ----
|
|
78
|
+
log "[2/8] inventorying policy files"
|
|
79
|
+
declare -A POLICY_FILES
|
|
80
|
+
# Map: filename → display name. Try multiple paths per file (root + .github/).
|
|
81
|
+
for entry in \
|
|
82
|
+
"CONTRIBUTING.md:CONTRIBUTING" \
|
|
83
|
+
".github/CONTRIBUTING.md:CONTRIBUTING" \
|
|
84
|
+
"CODE_OF_CONDUCT.md:CODE_OF_CONDUCT" \
|
|
85
|
+
".github/CODE_OF_CONDUCT.md:CODE_OF_CONDUCT" \
|
|
86
|
+
"SECURITY.md:SECURITY" \
|
|
87
|
+
".github/SECURITY.md:SECURITY" \
|
|
88
|
+
"CLA.md:CLA" \
|
|
89
|
+
"CLA.txt:CLA" \
|
|
90
|
+
"DCO.md:DCO" \
|
|
91
|
+
"DCO.txt:DCO" \
|
|
92
|
+
"AI_POLICY.md:AI_POLICY" \
|
|
93
|
+
"GOVERNANCE.md:GOVERNANCE" \
|
|
94
|
+
"SETUP.md:SETUP" \
|
|
95
|
+
"DEVELOPMENT.md:DEVELOPMENT" \
|
|
96
|
+
".github/CODEOWNERS:CODEOWNERS" \
|
|
97
|
+
"CODEOWNERS:CODEOWNERS" \
|
|
98
|
+
".github/PULL_REQUEST_TEMPLATE.md:PR_TEMPLATE" \
|
|
99
|
+
".github/pull_request_template.md:PR_TEMPLATE" \
|
|
100
|
+
"PULL_REQUEST_TEMPLATE.md:PR_TEMPLATE" \
|
|
101
|
+
".github/ISSUE_TEMPLATE.md:ISSUE_TEMPLATE_LEGACY" \
|
|
102
|
+
".github/issue_template.md:ISSUE_TEMPLATE_LEGACY" \
|
|
103
|
+
"ISSUE_TEMPLATE.md:ISSUE_TEMPLATE_LEGACY"
|
|
104
|
+
do
|
|
105
|
+
PATH_PART="${entry%:*}"
|
|
106
|
+
KEY="${entry##*:}"
|
|
107
|
+
# Skip if we already found this kind of file
|
|
108
|
+
[[ -n "${POLICY_FILES[$KEY]:-}" ]] && continue
|
|
109
|
+
|
|
110
|
+
# Probe by exit code, not by output. Earlier implementation captured stdout
|
|
111
|
+
# and tested for non-empty — but `gh api` on 404 prints the error-shaped
|
|
112
|
+
# JSON body ("{\"message\":\"Not Found\",...}") to stdout BEFORE jq runs,
|
|
113
|
+
# and the existing `2>/dev/null` only silences stderr. Net effect: EXISTS
|
|
114
|
+
# was non-empty for every probe, so every candidate file was claimed to
|
|
115
|
+
# exist regardless of reality. Fix: redirect stdout to /dev/null too and
|
|
116
|
+
# rely on the gh exit code as the existence signal.
|
|
117
|
+
if gh api "repos/$REPO/contents/$PATH_PART" >/dev/null 2>&1 ; then
|
|
118
|
+
POLICY_FILES[$KEY]="$PATH_PART"
|
|
119
|
+
fi
|
|
120
|
+
done
|
|
121
|
+
|
|
122
|
+
# Also probe `docs/` subdir for projects (like secureblue) that house policy
|
|
123
|
+
# docs there instead of at the repo root. Only fills in slots that are still
|
|
124
|
+
# empty after the root + .github/ probes above.
|
|
125
|
+
for entry in \
|
|
126
|
+
"docs/CONTRIBUTING.md:CONTRIBUTING" \
|
|
127
|
+
"docs/CODE_OF_CONDUCT.md:CODE_OF_CONDUCT" \
|
|
128
|
+
"docs/SECURITY.md:SECURITY" \
|
|
129
|
+
"docs/CLA.md:CLA" \
|
|
130
|
+
"docs/DCO.md:DCO" \
|
|
131
|
+
"docs/AI_POLICY.md:AI_POLICY" \
|
|
132
|
+
"docs/GOVERNANCE.md:GOVERNANCE" \
|
|
133
|
+
"docs/SETUP.md:SETUP" \
|
|
134
|
+
"docs/DEVELOPMENT.md:DEVELOPMENT" \
|
|
135
|
+
"docs/PULL_REQUEST_TEMPLATE.md:PR_TEMPLATE"
|
|
136
|
+
do
|
|
137
|
+
PATH_PART="${entry%:*}"
|
|
138
|
+
KEY="${entry##*:}"
|
|
139
|
+
[[ -n "${POLICY_FILES[$KEY]:-}" ]] && continue
|
|
140
|
+
if gh api "repos/$REPO/contents/$PATH_PART" >/dev/null 2>&1 ; then
|
|
141
|
+
POLICY_FILES[$KEY]="$PATH_PART"
|
|
142
|
+
fi
|
|
143
|
+
done
|
|
144
|
+
|
|
145
|
+
# ---- 2b. Issue template directory inventory (.github/ISSUE_TEMPLATE/) ----
|
|
146
|
+
# A repo can have a single legacy template (handled above) OR a directory of
|
|
147
|
+
# templates (the modern pattern: bug-report.md, feature-request.md, design.md, etc.).
|
|
148
|
+
# We list every .md file in that dir so SKILL.md can pick the right one when
|
|
149
|
+
# drafting a Design Issue or feature request.
|
|
150
|
+
log "[2b/8] inventorying issue template directory"
|
|
151
|
+
declare -a ISSUE_TEMPLATES=()
|
|
152
|
+
ISSUE_TEMPLATE_DIR_LISTING=$(gh api "repos/$REPO/contents/.github/ISSUE_TEMPLATE" 2>/dev/null \
|
|
153
|
+
| jq -r 'if type == "array" then .[] | select(.type == "file" and (.name | endswith(".md") or endswith(".yml") or endswith(".yaml"))) | .name else empty end' 2>/dev/null \
|
|
154
|
+
|| /usr/bin/echo "")
|
|
155
|
+
if [[ -n "$ISSUE_TEMPLATE_DIR_LISTING" ]] ; then
|
|
156
|
+
while read -r TPL ; do
|
|
157
|
+
[[ -n "$TPL" ]] && ISSUE_TEMPLATES+=("$TPL")
|
|
158
|
+
done <<< "$ISSUE_TEMPLATE_DIR_LISTING"
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
# ---- 3. Fetch CONTRIBUTING raw + extract links ----
|
|
162
|
+
log "[3/8] fetching + parsing CONTRIBUTING"
|
|
163
|
+
CONTRIB_RAW="$TMPDIR/contributing.md"
|
|
164
|
+
CONTRIB_PATH="${POLICY_FILES[CONTRIBUTING]:-}"
|
|
165
|
+
if [[ -n "$CONTRIB_PATH" ]] ; then
|
|
166
|
+
gh api "repos/$REPO/contents/$CONTRIB_PATH" --jq '.content' 2>/dev/null \
|
|
167
|
+
| /usr/bin/base64 -d > "$CONTRIB_RAW" 2>/dev/null \
|
|
168
|
+
|| /usr/bin/printf '' > "$CONTRIB_RAW"
|
|
169
|
+
fi
|
|
170
|
+
CONTRIB_BYTES=$(/usr/bin/wc -c < "$CONTRIB_RAW" 2>/dev/null || echo 0)
|
|
171
|
+
|
|
172
|
+
# Extract http(s) links from CONTRIBUTING — for depth-1 follow
|
|
173
|
+
LINKS_FILE="$TMPDIR/links.txt"
|
|
174
|
+
if [[ -s "$CONTRIB_RAW" ]] ; then
|
|
175
|
+
/usr/bin/grep -oE 'https?://[A-Za-z0-9._/?&=#%~+:-]+' "$CONTRIB_RAW" \
|
|
176
|
+
| /usr/bin/sed 's/[).,;:!]*$//' \
|
|
177
|
+
| /usr/bin/sort -u > "$LINKS_FILE"
|
|
178
|
+
else
|
|
179
|
+
: > "$LINKS_FILE"
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
# ---- 4. Follow depth-1 links — aggressively (per user direction 2026-05-02) ----
|
|
183
|
+
# Init array empty (set -u trip otherwise on `${#FOLLOWED_LINKS[@]}`).
|
|
184
|
+
declare -a FOLLOWED_LINKS=()
|
|
185
|
+
declare -A LINK_TITLES=()
|
|
186
|
+
if [[ "$NO_LINK_FOLLOW" != "--no-link-follow" && -s "$LINKS_FILE" ]] ; then
|
|
187
|
+
log "[4/8] depth-1 follow on links (skip social/external)"
|
|
188
|
+
while read -r URL ; do
|
|
189
|
+
# Skip social/external (twitter, x, discord, mailto, slack, youtube, linkedin, etc.)
|
|
190
|
+
if /usr/bin/echo "$URL" | /usr/bin/grep -qiE 'twitter\.com|x\.com|discord\.gg|discord\.com|mailto:|slack\.com|youtube\.com|youtu\.be|linkedin\.com|facebook\.com|instagram\.com'; then
|
|
191
|
+
continue
|
|
192
|
+
fi
|
|
193
|
+
# Skip GitHub anchor-only URLs (already covered by repo file scan)
|
|
194
|
+
[[ "$URL" == *"#"* ]] && continue
|
|
195
|
+
# Cap at 15 follows to keep cost bounded (was 5; user wanted aggressive)
|
|
196
|
+
[[ "${#FOLLOWED_LINKS[@]}" -ge 15 ]] && break
|
|
197
|
+
BODY=$(/usr/bin/curl -sSL --max-time 10 -A 'researcher-build/1.0' "$URL" 2>/dev/null | /usr/bin/head -c 50000)
|
|
198
|
+
if [[ -n "$BODY" && "$BODY" != *"404: Not Found"* ]] ; then
|
|
199
|
+
FOLLOWED_LINKS+=("$URL")
|
|
200
|
+
# Extract <title> tag (case-insensitive, strip whitespace, cap length).
|
|
201
|
+
# Falls back to the URL when no title element present.
|
|
202
|
+
TITLE=$(/usr/bin/printf '%s' "$BODY" \
|
|
203
|
+
| /usr/bin/grep -ioE '<title[^>]*>[^<]*</title>' \
|
|
204
|
+
| /usr/bin/head -1 \
|
|
205
|
+
| /usr/bin/sed -E 's|<title[^>]*>||I; s|</title>||I; s/^[[:space:]]+//; s/[[:space:]]+$//' \
|
|
206
|
+
| /usr/bin/cut -c1-120)
|
|
207
|
+
LINK_TITLES[$URL]="${TITLE:-$URL}"
|
|
208
|
+
/usr/bin/printf '%s' "$BODY" > "$TMPDIR/link-$(/usr/bin/echo "$URL" | /usr/bin/md5sum | /usr/bin/cut -c1-8).html"
|
|
209
|
+
fi
|
|
210
|
+
done < "$LINKS_FILE"
|
|
211
|
+
else
|
|
212
|
+
log "[4/8] link follow disabled"
|
|
213
|
+
fi
|
|
214
|
+
|
|
215
|
+
# ---- 5. External merge friendliness (last 90d) ----
|
|
216
|
+
log "[5/8] computing merge friendliness"
|
|
217
|
+
PRS_FILE="$TMPDIR/prs.json"
|
|
218
|
+
gh api "repos/$REPO/pulls?state=closed&sort=updated&direction=desc&per_page=100" > "$PRS_FILE" 2>/dev/null \
|
|
219
|
+
|| /usr/bin/printf '[]' > "$PRS_FILE"
|
|
220
|
+
EXT_COUNT=$(jq --arg cutoff "$NINETY_AGO" '
|
|
221
|
+
[.[]
|
|
222
|
+
| select(.merged_at != null)
|
|
223
|
+
| select((.merged_at|fromdateiso8601) >= ($cutoff|tonumber))
|
|
224
|
+
| select(.author_association == "CONTRIBUTOR" or .author_association == "FIRST_TIME_CONTRIBUTOR" or .author_association == "FIRST_TIMER" or .author_association == "NONE")
|
|
225
|
+
| select(.user.type != "Bot")
|
|
226
|
+
] | length' < "$PRS_FILE" 2>/dev/null || echo 0)
|
|
227
|
+
LAST_EXT=$(jq -r '
|
|
228
|
+
[.[]
|
|
229
|
+
| select(.merged_at != null)
|
|
230
|
+
| select(.author_association == "CONTRIBUTOR" or .author_association == "FIRST_TIME_CONTRIBUTOR" or .author_association == "NONE")
|
|
231
|
+
| select(.user.type != "Bot")
|
|
232
|
+
] | sort_by(.merged_at) | reverse | .[0].merged_at // ""
|
|
233
|
+
| if . == "" then "(none)" else .[0:10] end' < "$PRS_FILE" 2>/dev/null || echo "(err)")
|
|
234
|
+
|
|
235
|
+
# ---- 6. Bot detection (sample most-recently-updated merged PR) ----
|
|
236
|
+
log "[6/8] sampling review bots from a recent merged PR"
|
|
237
|
+
RECENT_PR=$(jq -r '[.[] | select(.merged_at != null)] | .[0].number // empty' < "$PRS_FILE" 2>/dev/null)
|
|
238
|
+
declare -a REVIEW_BOTS=() # init empty (set -u)
|
|
239
|
+
if [[ -n "$RECENT_PR" ]] ; then
|
|
240
|
+
REVIEWERS=$(gh pr view "$RECENT_PR" --repo "$REPO" --json reviews,comments \
|
|
241
|
+
--jq '([.reviews[].author.login] + [.comments[].author.login]) | unique' 2>/dev/null)
|
|
242
|
+
if [[ -n "$REVIEWERS" ]] ; then
|
|
243
|
+
while read -r LOGIN ; do
|
|
244
|
+
# Bot heuristic: ends in -bot, starts with app/, contains "bot" or "copilot" or "greptile" or "coderabbit" or "renovate" or "dependabot"
|
|
245
|
+
if /usr/bin/echo "$LOGIN" | /usr/bin/grep -qiE 'bot$|^app/|copilot|greptile|coderabbit|renovate|dependabot|github-actions|deploy-status' ; then
|
|
246
|
+
REVIEW_BOTS+=("$LOGIN")
|
|
247
|
+
fi
|
|
248
|
+
done < <(/usr/bin/echo "$REVIEWERS" | jq -r '.[]')
|
|
249
|
+
fi
|
|
250
|
+
fi
|
|
251
|
+
|
|
252
|
+
# ---- 7. Convention detection ----
|
|
253
|
+
log "[7/8] detecting conventions from CONTRIBUTING + linked pages"
|
|
254
|
+
ALL_TEXT="$TMPDIR/all-text.txt"
|
|
255
|
+
{ /usr/bin/cat "$CONTRIB_RAW" 2>/dev/null ; /usr/bin/cat "$TMPDIR"/link-*.html 2>/dev/null ; } > "$ALL_TEXT"
|
|
256
|
+
|
|
257
|
+
detect() { /usr/bin/grep -qiE "$1" "$ALL_TEXT" 2>/dev/null && echo "true" || echo "false" ; }
|
|
258
|
+
|
|
259
|
+
CLA_REQUIRED=$(detect '\b(CLA|contributor license agreement)\b')
|
|
260
|
+
DCO_REQUIRED=$(detect '\b(DCO|developer certificate of origin|signed-off-by|sign-off)\b')
|
|
261
|
+
AI_DISCLOSURE=$(detect '\b(AI[-_ ]?(generated|assisted|policy|disclos)|Claude|Copilot|ChatGPT|LLM)\b')
|
|
262
|
+
CONVENTIONAL_COMMITS=$(detect '\bconventional commits?\b|^[a-z]+\([a-z-]+\): ')
|
|
263
|
+
ETIQUETTE_REQUIRED=$(detect 'comment on the issue|request assignment|let.{0,20}know you.{0,5}working|don.{0,3}t.{0,10}assign')
|
|
264
|
+
# Try to grab a test command pattern
|
|
265
|
+
TEST_CMD=$(/usr/bin/grep -ioE '(make|cargo|pnpm|yarn|npm|pytest|sbt|go) (test|test-cov|lint|format-check|typecheck|check)[^`\n]*' "$ALL_TEXT" 2>/dev/null | /usr/bin/head -1 | /usr/bin/sed 's/[`"]//g' | /usr/bin/cut -c1-100)
|
|
266
|
+
|
|
267
|
+
# ---- 8. Emit the dossier ----
|
|
268
|
+
log "[8/8] emitting dossier ($CONTRIB_BYTES bytes CONTRIBUTING, ${#FOLLOWED_LINKS[@]} links followed, $EXT_COUNT ext merges)"
|
|
269
|
+
|
|
270
|
+
policy_list_yaml() {
|
|
271
|
+
for KEY in "${!POLICY_FILES[@]}" ; do
|
|
272
|
+
/usr/bin/printf ' - %s: %s\n' "$KEY" "${POLICY_FILES[$KEY]}"
|
|
273
|
+
done | /usr/bin/sort
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
bot_list_yaml() {
|
|
277
|
+
if [[ "${#REVIEW_BOTS[@]}" -eq 0 ]] ; then
|
|
278
|
+
/usr/bin/printf ' - (none detected)\n'
|
|
279
|
+
else
|
|
280
|
+
for B in "${REVIEW_BOTS[@]}" ; do
|
|
281
|
+
/usr/bin/printf ' - %s\n' "$B"
|
|
282
|
+
done | /usr/bin/sort -u
|
|
283
|
+
fi
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
issue_templates_section() {
|
|
287
|
+
if [[ "${#ISSUE_TEMPLATES[@]}" -eq 0 ]] ; then
|
|
288
|
+
if [[ -n "${POLICY_FILES[ISSUE_TEMPLATE_LEGACY]:-}" ]] ; then
|
|
289
|
+
local TPL="${POLICY_FILES[ISSUE_TEMPLATE_LEGACY]}"
|
|
290
|
+
/usr/bin/printf -- '- Legacy single-template at `%s` ([view](https://github.com/%s/blob/%s/%s)) — fetch this and fill it in.\n' \
|
|
291
|
+
"$TPL" "$REPO" "$DEFAULT_BRANCH" "$TPL"
|
|
292
|
+
else
|
|
293
|
+
/usr/bin/echo "_No issue templates detected. The repo accepts free-form issue bodies. Fall back to a generic Design MD shape (problem / proposal / diff preview / test results)._"
|
|
294
|
+
fi
|
|
295
|
+
else
|
|
296
|
+
/usr/bin/echo "_When opening a Design Issue / bug / feature request, **fetch the matching template first** and fill in its sections — do NOT replace the structure. The repo's reviewers expect this shape._"
|
|
297
|
+
/usr/bin/echo
|
|
298
|
+
for T in "${ISSUE_TEMPLATES[@]}" ; do
|
|
299
|
+
/usr/bin/printf -- '- `%s` — [view](https://github.com/%s/blob/%s/.github/ISSUE_TEMPLATE/%s)\n' \
|
|
300
|
+
"$T" "$REPO" "$DEFAULT_BRANCH" "$T"
|
|
301
|
+
done
|
|
302
|
+
/usr/bin/echo
|
|
303
|
+
/usr/bin/echo "**To fetch a template body for filling in:**"
|
|
304
|
+
/usr/bin/echo
|
|
305
|
+
/usr/bin/echo '```bash'
|
|
306
|
+
/usr/bin/printf 'gh api "repos/%s/contents/.github/ISSUE_TEMPLATE/<name>" --jq .content | base64 -d\n' "$REPO"
|
|
307
|
+
/usr/bin/echo '```'
|
|
308
|
+
fi
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
linked_sources_yaml() {
|
|
312
|
+
if [[ "${#FOLLOWED_LINKS[@]}" -eq 0 ]] ; then
|
|
313
|
+
/usr/bin/printf ' - (none followed)\n'
|
|
314
|
+
else
|
|
315
|
+
for U in "${FOLLOWED_LINKS[@]}" ; do
|
|
316
|
+
T="${LINK_TITLES[$U]:-$U}"
|
|
317
|
+
# Escape double quotes for YAML safety.
|
|
318
|
+
T_ESC="${T//\"/\\\"}"
|
|
319
|
+
/usr/bin/printf ' - { url: "%s", title: "%s", fetched_at: "%s" }\n' "$U" "$T_ESC" "$NOW"
|
|
320
|
+
done
|
|
321
|
+
fi
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
# Resolve the output path. Default: ~/.contribute-system/research/<owner>__<repo>.md.
|
|
325
|
+
# Override via CONTRIBUTE_RESEARCH_DIR. The path is stable per repo so refresh
|
|
326
|
+
# overwrites the previous dossier — engineer-curated sections (## Pet peeves,
|
|
327
|
+
# ## Failure log, ## Notes) survive because the agent layer copies them
|
|
328
|
+
# forward; this script does not preserve them across refresh.
|
|
329
|
+
RESEARCH_DIR="${CONTRIBUTE_RESEARCH_DIR:-$HOME/.contribute-system/research}"
|
|
330
|
+
OUTPUT_FILE="$RESEARCH_DIR/$(/usr/bin/echo "$REPO" | /usr/bin/sed 's|/|__|').md"
|
|
331
|
+
|
|
332
|
+
# When not in --stdout mode, redirect all subsequent stdout to the dossier
|
|
333
|
+
# file. The two heredocs (DOSSIER and TAIL) and the awk excerpt block in
|
|
334
|
+
# between all write to stdout — `exec` here points stdout at the file once,
|
|
335
|
+
# so every subsequent emission lands in the right place.
|
|
336
|
+
if [[ "$TO_STDOUT" -eq 0 ]]; then
|
|
337
|
+
/usr/bin/mkdir -p "$RESEARCH_DIR"
|
|
338
|
+
exec > "$OUTPUT_FILE"
|
|
339
|
+
fi
|
|
340
|
+
|
|
341
|
+
/usr/bin/cat <<DOSSIER
|
|
342
|
+
---
|
|
343
|
+
repo: $REPO
|
|
344
|
+
last_refreshed: $NOW
|
|
345
|
+
default_branch: $DEFAULT_BRANCH
|
|
346
|
+
archived: $ARCHIVED
|
|
347
|
+
stars: $STARS
|
|
348
|
+
language: $LANG
|
|
349
|
+
license: $LICENSE
|
|
350
|
+
last_pushed_at: $PUSHED_AT
|
|
351
|
+
external_merge_rate_90d: $EXT_COUNT
|
|
352
|
+
last_external_merge_at: $LAST_EXT
|
|
353
|
+
cla_required: $CLA_REQUIRED
|
|
354
|
+
dco_required: $DCO_REQUIRED
|
|
355
|
+
ai_disclosure_required: $AI_DISCLOSURE
|
|
356
|
+
conventional_commits: $CONVENTIONAL_COMMITS
|
|
357
|
+
etiquette_comment_required: $ETIQUETTE_REQUIRED
|
|
358
|
+
local_check_command: "${TEST_CMD:-(not detected)}"
|
|
359
|
+
policy_files:
|
|
360
|
+
$(policy_list_yaml)
|
|
361
|
+
issue_templates:
|
|
362
|
+
$(if [[ "${#ISSUE_TEMPLATES[@]}" -eq 0 ]] ; then
|
|
363
|
+
/usr/bin/printf ' - (none — repo has no .github/ISSUE_TEMPLATE/ dir)\n'
|
|
364
|
+
else
|
|
365
|
+
for T in "${ISSUE_TEMPLATES[@]}" ; do
|
|
366
|
+
/usr/bin/printf -- ' - { name: "%s", url: "https://github.com/%s/blob/%s/.github/ISSUE_TEMPLATE/%s" }\n' "$T" "$REPO" "$DEFAULT_BRANCH" "$T"
|
|
367
|
+
done
|
|
368
|
+
fi)
|
|
369
|
+
review_bots:
|
|
370
|
+
$(bot_list_yaml)
|
|
371
|
+
linked_sources:
|
|
372
|
+
$(linked_sources_yaml)
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
# $REPO — rules of engagement
|
|
376
|
+
|
|
377
|
+
> Auto-generated by researcher-build.sh on $NOW. Refresh with \`@researcher refresh $REPO\` (or any time CONTRIBUTING changes upstream).
|
|
378
|
+
|
|
379
|
+
## TL;DR
|
|
380
|
+
|
|
381
|
+
- $STARS ★ · $LANG · license: $LICENSE · default branch: \`$DEFAULT_BRANCH\`
|
|
382
|
+
- External merge velocity: **$EXT_COUNT** PRs in last 90d (last: $LAST_EXT)
|
|
383
|
+
- CLA: **$CLA_REQUIRED** · DCO: **$DCO_REQUIRED** · AI disclosure required: **$AI_DISCLOSURE**
|
|
384
|
+
- Etiquette comment required before claiming: **$ETIQUETTE_REQUIRED**
|
|
385
|
+
- Conventional Commits style: **$CONVENTIONAL_COMMITS**
|
|
386
|
+
- Local pre-PR command (detected): \`${TEST_CMD:-(none — read CONTRIBUTING)}\`
|
|
387
|
+
|
|
388
|
+
## Description
|
|
389
|
+
|
|
390
|
+
$DESC
|
|
391
|
+
|
|
392
|
+
## Policy file inventory
|
|
393
|
+
|
|
394
|
+
$(for KEY in "${!POLICY_FILES[@]}" ; do /usr/bin/printf -- '- **%s** → \`%s\` ([view](https://github.com/%s/blob/%s/%s))\n' "$KEY" "${POLICY_FILES[$KEY]}" "$REPO" "$DEFAULT_BRANCH" "${POLICY_FILES[$KEY]}" ; done | /usr/bin/sort)
|
|
395
|
+
|
|
396
|
+
## CONTRIBUTING.md — key excerpts
|
|
397
|
+
|
|
398
|
+
DOSSIER
|
|
399
|
+
|
|
400
|
+
# Excerpt the most actionable headed sections from CONTRIBUTING
|
|
401
|
+
if [[ -s "$CONTRIB_RAW" ]] ; then
|
|
402
|
+
# Pull sections with names that hint at "things you must do"
|
|
403
|
+
/usr/bin/awk '
|
|
404
|
+
/^##+ / { keep = 0 }
|
|
405
|
+
/^##+ .*([Cc]hecklist|[Pp]re-PR|[Bb]efore|[Tt]esting|[Tt]est|[Ww]orkflow|[Pp]ull [Rr]equest|[Bb]ranch|[Cc]ommit|[Ss]tyle|[Ff]ormat|[Aa]I|[Cc]ode [Qq]uality|[Cc]onvention|[Hh]ow to [Cc]ontribute|[Rr]eview|[Cc]ontribution [Ff]low|[Rr]ules)/ { keep = 1 }
|
|
406
|
+
keep { print }
|
|
407
|
+
' "$CONTRIB_RAW" | /usr/bin/head -150
|
|
408
|
+
/usr/bin/echo
|
|
409
|
+
/usr/bin/echo "_(excerpt only — full file: https://github.com/$REPO/blob/$DEFAULT_BRANCH/$CONTRIB_PATH)_"
|
|
410
|
+
else
|
|
411
|
+
/usr/bin/echo "_No CONTRIBUTING.md found at the repo root or .github/. Read CODE_OF_CONDUCT.md and the PR template instead._"
|
|
412
|
+
fi
|
|
413
|
+
|
|
414
|
+
/usr/bin/cat <<TAIL
|
|
415
|
+
|
|
416
|
+
## Linked sources (depth-1 follow)
|
|
417
|
+
|
|
418
|
+
$(if [[ "${#FOLLOWED_LINKS[@]}" -eq 0 ]] ; then
|
|
419
|
+
/usr/bin/echo "_No links followed (none found in CONTRIBUTING)._"
|
|
420
|
+
else
|
|
421
|
+
for U in "${FOLLOWED_LINKS[@]}" ; do
|
|
422
|
+
T="${LINK_TITLES[$U]:-$U}"
|
|
423
|
+
/usr/bin/printf -- '- [%s](%s) — fetched %s\n' "$T" "$U" "$NOW"
|
|
424
|
+
done
|
|
425
|
+
fi)
|
|
426
|
+
|
|
427
|
+
## Issue templates (use the matching one when opening a Design Issue or bug)
|
|
428
|
+
|
|
429
|
+
$(issue_templates_section)
|
|
430
|
+
|
|
431
|
+
## Bots that auto-review on this repo (sampled from PR #${RECENT_PR:-?})
|
|
432
|
+
|
|
433
|
+
$(bot_list_yaml | /usr/bin/sed 's/^ - /- /')
|
|
434
|
+
|
|
435
|
+
## Pet peeves & known triggers
|
|
436
|
+
|
|
437
|
+
_Specific things that get PRs closed at THIS repo. Manually + LLM-curated. Survives refresh — researcher does NOT auto-populate this section. Add an entry every time you observe a pet peeve in the wild._
|
|
438
|
+
|
|
439
|
+
- _(no entries yet — populate as observations land)_
|
|
440
|
+
|
|
441
|
+
## Failure log
|
|
442
|
+
|
|
443
|
+
_Chronological record of past closures with reasons at this repo. Auto-appended on \`status: dropped\` transitions; never overwritten on refresh._
|
|
444
|
+
|
|
445
|
+
## Notes
|
|
446
|
+
|
|
447
|
+
_Free-form area for the human to leave per-repo intuition. Survives refresh._
|
|
448
|
+
|
|
449
|
+
TAIL
|
|
450
|
+
|
|
451
|
+
# Success message goes to stderr so it's visible when stdout was redirected
|
|
452
|
+
# to the file. In --stdout mode, the user sees it inline before the dossier
|
|
453
|
+
# content (no — actually after, since heredocs flush before the script exits).
|
|
454
|
+
if [[ "$TO_STDOUT" -eq 0 ]]; then
|
|
455
|
+
log "→ wrote dossier to $OUTPUT_FILE"
|
|
456
|
+
fi
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test-known-traps.sh — regression tests for the failure modes that motivated
|
|
3
|
+
# the gate system. Each test constructs a synthetic candidate file matching a
|
|
4
|
+
# known real-world trap and asserts the expected gate fires with the expected
|
|
5
|
+
# severity.
|
|
6
|
+
#
|
|
7
|
+
# Usage: test-known-traps.sh [--verbose]
|
|
8
|
+
# Exit 0: all tests pass. Exit 1: any test fails.
|
|
9
|
+
#
|
|
10
|
+
# Tests:
|
|
11
|
+
# 1. PostHog #55412 — already-shipped issue (gate A02 must BLOCK)
|
|
12
|
+
# 2. opensre #1129 — assigned issue (gate A01 must BLOCK)
|
|
13
|
+
# 3. closed issue — closed-not-planned (gate A05 must BLOCK)
|
|
14
|
+
# 4. clean candidate — no traps (transition must PASS or only SKIP)
|
|
15
|
+
|
|
16
|
+
set -uo pipefail
|
|
17
|
+
|
|
18
|
+
VERBOSE="${1:-}"
|
|
19
|
+
SYS="$HOME/.contribute-system"
|
|
20
|
+
TMPDIR=$(/usr/bin/mktemp -d)
|
|
21
|
+
trap 'rm -rf "$TMPDIR"' EXIT
|
|
22
|
+
|
|
23
|
+
PASS=0
|
|
24
|
+
FAIL=0
|
|
25
|
+
RESULTS=()
|
|
26
|
+
|
|
27
|
+
red() { /usr/bin/printf '\033[31m%s\033[0m' "$1"; }
|
|
28
|
+
green() { /usr/bin/printf '\033[32m%s\033[0m' "$1"; }
|
|
29
|
+
yellow() { /usr/bin/printf '\033[33m%s\033[0m' "$1"; }
|
|
30
|
+
|
|
31
|
+
# Helper: run transition.sh in dry-run mode against a candidate.
|
|
32
|
+
# Returns the verdict JSON on stdout.
|
|
33
|
+
run_transition() {
|
|
34
|
+
local action="$1" candidate="$2"
|
|
35
|
+
"$SYS/bin/transition.sh" "$action" "$candidate" --dry-run 2>/dev/null \
|
|
36
|
+
| /usr/bin/grep -E '^\{.*"verdict"' \
|
|
37
|
+
| /usr/bin/tail -1
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Helper: assert a specific gate ID appears with a specific severity in the
|
|
41
|
+
# verbose stderr output of transition.sh.
|
|
42
|
+
gate_fired_with_severity() {
|
|
43
|
+
local action="$1" candidate="$2" gate_id="$3" expected_sev="$4"
|
|
44
|
+
local stderr_output
|
|
45
|
+
stderr_output=$("$SYS/bin/transition.sh" "$action" "$candidate" --dry-run 2>&1 >/dev/null)
|
|
46
|
+
if [[ "$VERBOSE" == "--verbose" ]]; then
|
|
47
|
+
/usr/bin/printf '\n--- transition output for %s on %s ---\n%s\n---\n' \
|
|
48
|
+
"$action" "$(/usr/bin/basename "$candidate")" "$stderr_output" >&2
|
|
49
|
+
fi
|
|
50
|
+
/usr/bin/echo "$stderr_output" | /usr/bin/grep -qE "\[$gate_id\].*$expected_sev"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
assert_gate() {
|
|
54
|
+
local name="$1" action="$2" candidate="$3" gate_id="$4" expected_sev="$5"
|
|
55
|
+
/usr/bin/printf ' %-50s ' "$name"
|
|
56
|
+
if gate_fired_with_severity "$action" "$candidate" "$gate_id" "$expected_sev"; then
|
|
57
|
+
green "PASS"; /usr/bin/echo
|
|
58
|
+
PASS=$((PASS + 1))
|
|
59
|
+
RESULTS+=("PASS: $name")
|
|
60
|
+
else
|
|
61
|
+
red "FAIL"; /usr/bin/echo
|
|
62
|
+
/usr/bin/printf ' expected gate %s severity %s — not found\n' "$gate_id" "$expected_sev" >&2
|
|
63
|
+
FAIL=$((FAIL + 1))
|
|
64
|
+
RESULTS+=("FAIL: $name (expected $gate_id $expected_sev)")
|
|
65
|
+
fi
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Build a synthetic candidate file. The frontmatter is what gates read.
|
|
69
|
+
# Body is unused for these tests.
|
|
70
|
+
make_candidate() {
|
|
71
|
+
local repo="$1" issue="$2" status="${3:-open}" path
|
|
72
|
+
path="$TMPDIR/synth_$(/usr/bin/echo "$repo" | /usr/bin/tr '/' '_')_$issue.md"
|
|
73
|
+
/usr/bin/cat > "$path" <<EOF
|
|
74
|
+
---
|
|
75
|
+
discovered_at: 2026-05-03T00:00:00Z
|
|
76
|
+
repo: $repo
|
|
77
|
+
issue_number: $issue
|
|
78
|
+
issue_url: https://github.com/$repo/issues/$issue
|
|
79
|
+
star_tier: mainstream
|
|
80
|
+
star_count: 1000
|
|
81
|
+
repo_lang: TypeScript
|
|
82
|
+
competing_prs: 0
|
|
83
|
+
primary_label: bug
|
|
84
|
+
scout_score: 0.5
|
|
85
|
+
status: $status
|
|
86
|
+
last_refreshed: 2026-05-03T00:00:00Z
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
# $repo #$issue — synthetic regression test candidate
|
|
90
|
+
EOF
|
|
91
|
+
/usr/bin/echo "$path"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/usr/bin/printf '\n=== contributing-clanker regression tests — known traps ===\n\n'
|
|
95
|
+
|
|
96
|
+
# ---- TEST 1: PostHog #55412 trap (already-shipped) ----
|
|
97
|
+
# The plan documents this as the originating trap for gate A02.
|
|
98
|
+
# Issue #55412 was closed by merged PR #57145. We need A02 to BLOCK.
|
|
99
|
+
/usr/bin/printf 'Test 1: PostHog #55412 already-shipped trap\n'
|
|
100
|
+
T1=$(make_candidate "PostHog/posthog" 55412)
|
|
101
|
+
assert_gate " A02 already-shipped MUST BLOCK" \
|
|
102
|
+
"shortlist→claimed" "$T1" "A02" "BLOCK"
|
|
103
|
+
|
|
104
|
+
# ---- TEST 2: Tracer-Cloud opensre #1129 trap (assigned) ----
|
|
105
|
+
# This issue was assigned to unKnownNG when scout found it. A01 must BLOCK.
|
|
106
|
+
/usr/bin/printf 'Test 2: Tracer-Cloud opensre #1129 assigned trap\n'
|
|
107
|
+
T2=$(make_candidate "Tracer-Cloud/opensre" 1129)
|
|
108
|
+
assert_gate " A01 already-assigned MUST BLOCK" \
|
|
109
|
+
"shortlist→claimed" "$T2" "A01" "BLOCK"
|
|
110
|
+
|
|
111
|
+
# ---- TEST 3: lingdojo/kana-dojo #15441 (closed not_planned) ----
|
|
112
|
+
# Verified empirically 2026-05-03 — A05 BLOCKs on closed issues.
|
|
113
|
+
/usr/bin/printf 'Test 3: lingdojo/kana-dojo #15441 closed-issue trap\n'
|
|
114
|
+
T3=$(make_candidate "lingdojo/kana-dojo" 15441)
|
|
115
|
+
assert_gate " A05 issue-still-open MUST BLOCK" \
|
|
116
|
+
"shortlist→claimed" "$T3" "A05" "BLOCK"
|
|
117
|
+
|
|
118
|
+
# ---- TEST 4: clean candidate (no traps) ----
|
|
119
|
+
# Pick a repo+issue that's currently OPEN and unassigned. We use a synthetic
|
|
120
|
+
# very-high issue number that won't match any real shipped PR.
|
|
121
|
+
# Note: this test runs against a real repo via gh; if the issue doesn't exist,
|
|
122
|
+
# the gates will produce ambiguous results. We pick something neutral.
|
|
123
|
+
/usr/bin/printf 'Test 4: clean candidate (open, unassigned, unshipped)\n'
|
|
124
|
+
T4=$(make_candidate "lingdojo/kana-dojo" 99999999)
|
|
125
|
+
# A01 should PASS or A03/A04/A05 — we just want NO unexpected BLOCKs from A1
|
|
126
|
+
# (which is the most common false-positive risk).
|
|
127
|
+
assert_gate " A01 should NOT block (issue 99999999 nonexistent)" \
|
|
128
|
+
"shortlist→claimed" "$T4" "A01" "(PASS|SKIP)"
|
|
129
|
+
|
|
130
|
+
/usr/bin/echo
|
|
131
|
+
/usr/bin/printf '=== summary: %s passed · %s failed ===\n\n' \
|
|
132
|
+
"$(green "$PASS")" "$([ "$FAIL" -gt 0 ] && red "$FAIL" || /usr/bin/echo 0)"
|
|
133
|
+
|
|
134
|
+
if [[ "$FAIL" -gt 0 ]]; then
|
|
135
|
+
/usr/bin/printf 'Failures:\n'
|
|
136
|
+
for R in "${RESULTS[@]}"; do
|
|
137
|
+
[[ "$R" == FAIL:* ]] && /usr/bin/printf ' %s\n' "$R"
|
|
138
|
+
done
|
|
139
|
+
exit 1
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
exit 0
|