@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,99 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# catalog-coverage.sh — report which catalog modes have gate implementations.
|
|
3
|
+
#
|
|
4
|
+
# Closes contributing-clanker-p5q.3.
|
|
5
|
+
#
|
|
6
|
+
# The 62-failure-mode catalog lives at:
|
|
7
|
+
# <repo>/000-docs/007-DR-CATG-failure-mode-catalog.md
|
|
8
|
+
# Each row maps a mode ID (e.g. A1, B12, C5) to a "Gate" column with either:
|
|
9
|
+
# - a real gate filename (e.g. "a01-already-assigned")
|
|
10
|
+
# - "(planned)" for unimplemented modes
|
|
11
|
+
# - "(see ...)" for modes covered by a different gate via reference
|
|
12
|
+
#
|
|
13
|
+
# Coverage = real-gate count / total catalog count.
|
|
14
|
+
# Output: per-phase table + overall %.
|
|
15
|
+
|
|
16
|
+
set -uo pipefail
|
|
17
|
+
|
|
18
|
+
CATALOG="${1:-${HOME}/000-projects/contributing-clanker/000-docs/007-DR-CATG-failure-mode-catalog.md}"
|
|
19
|
+
GATES_DIR="${HOME}/.claude/skills/contribute/scripts/gates"
|
|
20
|
+
|
|
21
|
+
if [[ ! -f "$CATALOG" ]]; then
|
|
22
|
+
/usr/bin/printf 'catalog not found: %s\n' "$CATALOG" >&2
|
|
23
|
+
/usr/bin/printf 'usage: %s [path-to-catalog.md]\n' "$0" >&2
|
|
24
|
+
exit 2
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
if [[ ! -d "$GATES_DIR" ]]; then
|
|
28
|
+
/usr/bin/printf 'gates dir not found: %s\n' "$GATES_DIR" >&2
|
|
29
|
+
exit 2
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# Extract catalog rows. Format: | A1 | description | trigger | gate-name |
|
|
33
|
+
# We only care about rows where col 1 is a single-letter+digit ID and col 4
|
|
34
|
+
# (last col) is the gate ref.
|
|
35
|
+
#
|
|
36
|
+
# Each markdown table row: | id | failure | trigger | gate |
|
|
37
|
+
# We want id (col 2 after leading |) and gate (col 5 after leading |).
|
|
38
|
+
parse_catalog() {
|
|
39
|
+
/usr/bin/awk -F'|' '
|
|
40
|
+
/^\| *[A-G][0-9]+ *\|/ {
|
|
41
|
+
gsub(/^[ \t]+|[ \t]+$/, "", $2) # mode id
|
|
42
|
+
gsub(/^[ \t]+|[ \t]+$/, "", $5) # gate ref
|
|
43
|
+
print $2 "\t" $5
|
|
44
|
+
}
|
|
45
|
+
' "$CATALOG"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Aggregate per phase (A,B,C,D,E,F,G)
|
|
49
|
+
declare -A phase_total phase_planned phase_implemented
|
|
50
|
+
total=0
|
|
51
|
+
implemented=0
|
|
52
|
+
planned=0
|
|
53
|
+
|
|
54
|
+
while IFS=$'\t' read -r id gate; do
|
|
55
|
+
phase="${id:0:1}"
|
|
56
|
+
phase_total[$phase]=$(( ${phase_total[$phase]:-0} + 1 ))
|
|
57
|
+
total=$(( total + 1 ))
|
|
58
|
+
case "$gate" in
|
|
59
|
+
*planned*)
|
|
60
|
+
phase_planned[$phase]=$(( ${phase_planned[$phase]:-0} + 1 ))
|
|
61
|
+
planned=$(( planned + 1 ))
|
|
62
|
+
;;
|
|
63
|
+
*)
|
|
64
|
+
# Real gate ref. Confirm a script file actually exists for it.
|
|
65
|
+
# Gate refs in the catalog look like "a01-already-assigned" — match by
|
|
66
|
+
# prefix lower-case + leading digits.
|
|
67
|
+
gate_id_normalized=$(/usr/bin/printf '%s' "$gate" | /usr/bin/tr '[:upper:]' '[:lower:]' | /usr/bin/sed 's/[^a-z0-9].*//')
|
|
68
|
+
if /usr/bin/find "$GATES_DIR" -maxdepth 1 -name "${gate_id_normalized}*.sh" -print -quit 2>/dev/null | /usr/bin/grep -q .; then
|
|
69
|
+
phase_implemented[$phase]=$(( ${phase_implemented[$phase]:-0} + 1 ))
|
|
70
|
+
implemented=$(( implemented + 1 ))
|
|
71
|
+
else
|
|
72
|
+
phase_planned[$phase]=$(( ${phase_planned[$phase]:-0} + 1 ))
|
|
73
|
+
planned=$(( planned + 1 ))
|
|
74
|
+
fi
|
|
75
|
+
;;
|
|
76
|
+
esac
|
|
77
|
+
done < <(parse_catalog)
|
|
78
|
+
|
|
79
|
+
# Render
|
|
80
|
+
/usr/bin/printf '\nCatalog → Gate coverage\n'
|
|
81
|
+
/usr/bin/printf '═════════════════════════════════════════\n'
|
|
82
|
+
/usr/bin/printf '%-8s %10s %12s %10s\n' "PHASE" "TOTAL" "IMPLEMENTED" "COVERAGE"
|
|
83
|
+
/usr/bin/printf '─────────────────────────────────────────\n'
|
|
84
|
+
for phase in A B C D E F G; do
|
|
85
|
+
t=${phase_total[$phase]:-0}
|
|
86
|
+
i=${phase_implemented[$phase]:-0}
|
|
87
|
+
if [[ $t -gt 0 ]]; then
|
|
88
|
+
pct=$(( i * 100 / t ))
|
|
89
|
+
/usr/bin/printf '%-8s %10s %12s %9s%%\n' "$phase" "$t" "$i" "$pct"
|
|
90
|
+
fi
|
|
91
|
+
done
|
|
92
|
+
/usr/bin/printf '─────────────────────────────────────────\n'
|
|
93
|
+
overall_pct=$(( implemented * 100 / total ))
|
|
94
|
+
/usr/bin/printf '%-8s %10s %12s %9s%%\n' "TOTAL" "$total" "$implemented" "$overall_pct"
|
|
95
|
+
/usr/bin/printf '═════════════════════════════════════════\n\n'
|
|
96
|
+
|
|
97
|
+
if [[ $planned -gt 0 ]]; then
|
|
98
|
+
/usr/bin/printf ' %d catalog mode(s) still planned (no gate file).\n\n' "$planned"
|
|
99
|
+
fi
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# gate-runner.sh — the orchestrator that runs gates for a lifecycle transition.
|
|
3
|
+
#
|
|
4
|
+
# Usage: gate-runner.sh <action> <candidate-path> [<dossier-path>]
|
|
5
|
+
# action: e.g., "shortlist→claimed", "working→submitted", "open-pr", "post-comment"
|
|
6
|
+
#
|
|
7
|
+
# Discovers gates by glob from ~/.contribute-system/gates/<phase>*.sh,
|
|
8
|
+
# filters by which gates apply to this action (per the gate's filename phase
|
|
9
|
+
# letter and the action's lifecycle stage), runs each in turn with a 10-second
|
|
10
|
+
# timeout, aggregates verdicts.
|
|
11
|
+
#
|
|
12
|
+
# Output: one JSON per gate to stderr (for human-readable progress);
|
|
13
|
+
# final aggregated verdict to stdout.
|
|
14
|
+
#
|
|
15
|
+
# Exit code: 0 if all PASS/WARN/INFORM/SKIP; 1 if any BLOCK (unless every
|
|
16
|
+
# BLOCK was overridden via the candidate's overrides: frontmatter).
|
|
17
|
+
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
|
|
20
|
+
ACTION="${1:-}"
|
|
21
|
+
CANDIDATE="${2:-}"
|
|
22
|
+
DOSSIER="${3:-}"
|
|
23
|
+
|
|
24
|
+
if [[ -z "$ACTION" || -z "$CANDIDATE" ]]; then
|
|
25
|
+
echo "usage: $0 <action> <candidate-path> [<dossier-path>]" >&2
|
|
26
|
+
exit 64
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# Discover gates from two dirs, in priority order:
|
|
30
|
+
# 1. Bundled canonical set at skill/scripts/gates/ (discovered via script location)
|
|
31
|
+
# 2. User-override gates at ~/.contribute-system/gates/ (personal additions / overrides)
|
|
32
|
+
# This split means the bundled set is always present (distributable) and users can
|
|
33
|
+
# add custom gates or fork existing ones without modifying the skill package.
|
|
34
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
35
|
+
BUNDLED_GATE_DIR="${SCRIPT_DIR}/gates"
|
|
36
|
+
USER_GATE_DIR="$HOME/.contribute-system/gates"
|
|
37
|
+
LOG="$HOME/.contribute-system/log.jsonl"
|
|
38
|
+
NOW=$(/usr/bin/date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
39
|
+
|
|
40
|
+
# Map action → relevant gate phases. Gates are filtered by phase letter
|
|
41
|
+
# (first char of filename). An action runs all gates in its applicable phases.
|
|
42
|
+
case "$ACTION" in
|
|
43
|
+
"open→shortlist") PHASES="A" ;;
|
|
44
|
+
"shortlist→claimed") PHASES="A E" ;;
|
|
45
|
+
"claimed→working") PHASES="A B" ;;
|
|
46
|
+
"working→submitted") PHASES="B C E F G" ;;
|
|
47
|
+
"open-pr") PHASES="C E" ;;
|
|
48
|
+
"flip-to-ready") PHASES="C" ;;
|
|
49
|
+
"post-comment") PHASES="D" ;;
|
|
50
|
+
"open-issue") PHASES="D" ;;
|
|
51
|
+
*) PHASES="A B C D E F G" ;; # unknown: run everything
|
|
52
|
+
esac
|
|
53
|
+
|
|
54
|
+
# Read disabled_gates from dossier (per-repo opt-out)
|
|
55
|
+
DISABLED=""
|
|
56
|
+
if [[ -n "$DOSSIER" && -f "$DOSSIER" ]]; then
|
|
57
|
+
DISABLED=$(/usr/bin/awk '/^---$/{fm=!fm?1:2;next} fm==1 && /^disabled_gates:/{
|
|
58
|
+
sub(/^disabled_gates:[[:space:]]*\[/,""); sub(/\][[:space:]]*$/,""); gsub(/[[:space:]]/,""); print; exit
|
|
59
|
+
}' "$DOSSIER" 2>/dev/null || /usr/bin/echo "")
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# Read repo + branch from candidate frontmatter
|
|
63
|
+
REPO=$(/usr/bin/awk '/^---$/{fm=!fm?1:2;next} fm==1 && /^repo:/{sub(/^repo:[[:space:]]*/,""); print; exit}' "$CANDIDATE" 2>/dev/null || /usr/bin/echo "")
|
|
64
|
+
BRANCH=$(/usr/bin/awk '/^---$/{fm=!fm?1:2;next} fm==1 && /^branch:/{sub(/^branch:[[:space:]]*/,""); print; exit}' "$CANDIDATE" 2>/dev/null || /usr/bin/echo "")
|
|
65
|
+
|
|
66
|
+
# Build the input JSON once (passed to every gate)
|
|
67
|
+
INPUT_JSON=$(jq -nc \
|
|
68
|
+
--arg candidate "$CANDIDATE" \
|
|
69
|
+
--arg dossier "$DOSSIER" \
|
|
70
|
+
--arg action "$ACTION" \
|
|
71
|
+
--arg repo "$REPO" \
|
|
72
|
+
--arg branch "$BRANCH" \
|
|
73
|
+
'{candidate: $candidate, dossier: $dossier, action: $action, env: {repo: $repo, branch: $branch}}')
|
|
74
|
+
|
|
75
|
+
# Discover gate scripts for the relevant phases.
|
|
76
|
+
# Bundled gates first, then user-override dir.
|
|
77
|
+
# A user gate with the same basename shadows the bundled one (by position in GATES array).
|
|
78
|
+
# gate-runner runs all discovered gates; SKIP is cheap.
|
|
79
|
+
declare -A SEEN_GATES=()
|
|
80
|
+
GATES=()
|
|
81
|
+
for DIR in "$BUNDLED_GATE_DIR" "$USER_GATE_DIR" ; do
|
|
82
|
+
[[ -d "$DIR" ]] || continue
|
|
83
|
+
for PHASE in $PHASES ; do
|
|
84
|
+
for GATE in "$DIR"/${PHASE,,}*.sh ; do
|
|
85
|
+
[[ -f "$GATE" && -x "$GATE" ]] || continue
|
|
86
|
+
BN=$(/usr/bin/basename "$GATE")
|
|
87
|
+
# User-override dir wins — if a gate with same name was already queued from
|
|
88
|
+
# bundled dir, replace it. Simple: add all, let user-dir overwrite.
|
|
89
|
+
[[ -z "${SEEN_GATES[$BN]:-}" ]] && GATES+=("$GATE") || true
|
|
90
|
+
SEEN_GATES[$BN]="$GATE"
|
|
91
|
+
done
|
|
92
|
+
done
|
|
93
|
+
done
|
|
94
|
+
|
|
95
|
+
if [[ "${#GATES[@]}" -eq 0 ]]; then
|
|
96
|
+
/usr/bin/echo '{"verdict":"PASS","gates_run":0,"reason":"no gates applicable for this action"}'
|
|
97
|
+
exit 0
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
# Run each gate. Aggregate.
|
|
101
|
+
PASS_COUNT=0
|
|
102
|
+
WARN_COUNT=0
|
|
103
|
+
BLOCK_COUNT=0
|
|
104
|
+
INFORM_COUNT=0
|
|
105
|
+
SKIP_COUNT=0
|
|
106
|
+
BLOCKERS=()
|
|
107
|
+
WARNINGS=()
|
|
108
|
+
|
|
109
|
+
/usr/bin/printf '\n=== gate-runner: %s — %d gates ===\n' "$ACTION" "${#GATES[@]}" >&2
|
|
110
|
+
|
|
111
|
+
for GATE in "${GATES[@]}" ; do
|
|
112
|
+
GATE_NAME=$(/usr/bin/basename "$GATE" .sh)
|
|
113
|
+
GATE_ID=$(/usr/bin/printf '%s' "$GATE_NAME" | /usr/bin/cut -d- -f1 | /usr/bin/tr 'a-z' 'A-Z')
|
|
114
|
+
|
|
115
|
+
# Per-repo opt-out check
|
|
116
|
+
if /usr/bin/echo ",$DISABLED," | /usr/bin/grep -qi ",$GATE_ID,"; then
|
|
117
|
+
/usr/bin/printf ' [%s] SKIP — disabled per dossier\n' "$GATE_ID" >&2
|
|
118
|
+
SKIP_COUNT=$(( SKIP_COUNT + 1 ))
|
|
119
|
+
continue
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
# Run the gate with timeout. Capture stdout. Exit non-zero = treat as BLOCK.
|
|
123
|
+
if VERDICT_JSON=$(/usr/bin/timeout 10 bash -c "/usr/bin/printf '%s' '$INPUT_JSON' | '$GATE'" 2>/dev/null); then
|
|
124
|
+
: # ok, parse verdict
|
|
125
|
+
else
|
|
126
|
+
VERDICT_JSON=$(jq -nc --arg gid "$GATE_ID" '{severity:"BLOCK", gate:$gid, reason:"gate timed out or crashed (>10s or non-zero exit) — fail-closed", fix_hint:"check the gate script for bugs; preamble.sh should have caught it"}')
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
# Defensive parse: if the gate returned malformed JSON, treat as BLOCK
|
|
130
|
+
if ! /usr/bin/printf '%s' "$VERDICT_JSON" | jq -e . >/dev/null 2>&1; then
|
|
131
|
+
VERDICT_JSON=$(jq -nc --arg gid "$GATE_ID" --arg raw "$VERDICT_JSON" '{severity:"BLOCK", gate:$gid, reason:"gate returned malformed JSON — fail-closed", fix_hint:("raw stdout: " + ($raw | tostring))}')
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
SEV=$(/usr/bin/printf '%s' "$VERDICT_JSON" | jq -r '.severity')
|
|
135
|
+
REASON=$(/usr/bin/printf '%s' "$VERDICT_JSON" | jq -r '.reason')
|
|
136
|
+
FIX=$(/usr/bin/printf '%s' "$VERDICT_JSON" | jq -r '.fix_hint // ""')
|
|
137
|
+
|
|
138
|
+
case "$SEV" in
|
|
139
|
+
PASS) PASS_COUNT=$((PASS_COUNT+1)); /usr/bin/printf ' [%s] \033[32mPASS\033[0m — %s\n' "$GATE_ID" "$REASON" >&2 ;;
|
|
140
|
+
WARN) WARN_COUNT=$((WARN_COUNT+1)); WARNINGS+=("$GATE_ID: $REASON ($FIX)") ; /usr/bin/printf ' [%s] \033[33mWARN\033[0m — %s\n' "$GATE_ID" "$REASON" >&2 ;;
|
|
141
|
+
BLOCK) BLOCK_COUNT=$((BLOCK_COUNT+1)); BLOCKERS+=("$GATE_ID: $REASON ($FIX)") ; /usr/bin/printf ' [%s] \033[31mBLOCK\033[0m — %s\n fix: %s\n' "$GATE_ID" "$REASON" "$FIX" >&2 ;;
|
|
142
|
+
INFORM) INFORM_COUNT=$((INFORM_COUNT+1)); /usr/bin/printf ' [%s] INFO — %s\n' "$GATE_ID" "$REASON" >&2 ;;
|
|
143
|
+
SKIP) SKIP_COUNT=$((SKIP_COUNT+1)); /usr/bin/printf ' [%s] SKIP — %s\n' "$GATE_ID" "$REASON" >&2 ;;
|
|
144
|
+
*) BLOCK_COUNT=$((BLOCK_COUNT+1)); BLOCKERS+=("$GATE_ID: unknown severity '$SEV'") ; /usr/bin/printf ' [%s] BLOCK — unknown severity: %s\n' "$GATE_ID" "$SEV" >&2 ;;
|
|
145
|
+
esac
|
|
146
|
+
|
|
147
|
+
# Append run to log
|
|
148
|
+
/usr/bin/printf '%s\n' "$(jq -nc \
|
|
149
|
+
--arg ts "$NOW" \
|
|
150
|
+
--arg gate "$GATE_ID" \
|
|
151
|
+
--arg action "$ACTION" \
|
|
152
|
+
--arg repo "$REPO" \
|
|
153
|
+
--arg sev "$SEV" \
|
|
154
|
+
--arg reason "$REASON" \
|
|
155
|
+
'{ts: $ts, event: "gate_run", details: {gate: $gate, action: $action, repo: $repo, severity: $sev, reason: $reason}}')" >> "$LOG" 2>/dev/null || true
|
|
156
|
+
done
|
|
157
|
+
|
|
158
|
+
# Check overrides on the candidate (any BLOCK gate the user explicitly waived).
|
|
159
|
+
OVERRIDDEN=()
|
|
160
|
+
if [[ -f "$CANDIDATE" ]] && /usr/bin/grep -q '^overrides:' "$CANDIDATE" 2>/dev/null; then
|
|
161
|
+
while IFS= read -r OG; do
|
|
162
|
+
OVERRIDDEN+=("$OG")
|
|
163
|
+
done < <(/usr/bin/awk '/^overrides:/{flag=1;next} /^[a-z_]+:/{flag=0} flag && /gate:/{sub(/.*gate:[[:space:]]*/,"");sub(/[[:space:]]*,.*$/,"");print}' "$CANDIDATE" 2>/dev/null)
|
|
164
|
+
fi
|
|
165
|
+
|
|
166
|
+
# Filter BLOCKERS against OVERRIDDEN
|
|
167
|
+
EFFECTIVE_BLOCKS=()
|
|
168
|
+
for B in "${BLOCKERS[@]}"; do
|
|
169
|
+
BID="${B%%:*}"
|
|
170
|
+
IS_OVERRIDDEN=0
|
|
171
|
+
for O in "${OVERRIDDEN[@]}"; do
|
|
172
|
+
[[ "$O" == "$BID" ]] && { IS_OVERRIDDEN=1; break; }
|
|
173
|
+
done
|
|
174
|
+
if [[ "$IS_OVERRIDDEN" -eq 0 ]]; then
|
|
175
|
+
EFFECTIVE_BLOCKS+=("$B")
|
|
176
|
+
fi
|
|
177
|
+
done
|
|
178
|
+
|
|
179
|
+
# Final verdict
|
|
180
|
+
TOTAL=${#GATES[@]}
|
|
181
|
+
EFFECTIVE_BLOCK_COUNT=${#EFFECTIVE_BLOCKS[@]}
|
|
182
|
+
/usr/bin/printf '\n=== summary: %d gates · %d PASS · %d WARN · %d BLOCK (%d after overrides) · %d INFORM · %d SKIP ===\n\n' \
|
|
183
|
+
"$TOTAL" "$PASS_COUNT" "$WARN_COUNT" "$BLOCK_COUNT" "$EFFECTIVE_BLOCK_COUNT" "$INFORM_COUNT" "$SKIP_COUNT" >&2
|
|
184
|
+
|
|
185
|
+
if [[ "$EFFECTIVE_BLOCK_COUNT" -gt 0 ]]; then
|
|
186
|
+
/usr/bin/echo "$(jq -nc --argjson b "$(printf '%s\n' "${EFFECTIVE_BLOCKS[@]}" | jq -R . | jq -s .)" --argjson w "$(printf '%s\n' "${WARNINGS[@]}" | jq -R . | jq -s .)" --argjson n "$EFFECTIVE_BLOCK_COUNT" '{verdict: "BLOCK", effective_blocks: $n, blockers: $b, warnings: $w}')"
|
|
187
|
+
exit 1
|
|
188
|
+
else
|
|
189
|
+
/usr/bin/echo "$(jq -nc --argjson w "$(printf '%s\n' "${WARNINGS[@]}" | jq -R . | jq -s .)" --argjson p "$PASS_COUNT" '{verdict: "PASS", gates_passed: $p, warnings: $w}')"
|
|
190
|
+
exit 0
|
|
191
|
+
fi
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Catalog: A1 — Claim already-assigned issue
|
|
3
|
+
# Mitigates: Tracer-Cloud opensre #1129 trap (2026-05-02) — issue assigned to
|
|
4
|
+
# unKnownNG day before our scout run; he was actively asking maintainer
|
|
5
|
+
# clarifying questions in comments. We almost claim-jumped real human work.
|
|
6
|
+
source "$(dirname "$0")/lib/preamble.sh"
|
|
7
|
+
|
|
8
|
+
gate_read_input
|
|
9
|
+
|
|
10
|
+
# Pull issue number from candidate frontmatter
|
|
11
|
+
ISSUE_NUM=$(fm_field "$GATE_CANDIDATE_PATH" "issue_number")
|
|
12
|
+
if [[ -z "$ISSUE_NUM" || -z "$GATE_REPO" ]]; then
|
|
13
|
+
gate_skip "no issue_number or repo in candidate"
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
# Live check (this is one of the few gates that MUST be live — assignment
|
|
17
|
+
# state changes too fast to trust the dossier)
|
|
18
|
+
ASSIGNEES=$(gh_safe issue view "$ISSUE_NUM" --repo "$GATE_REPO" --json assignees --jq '[.assignees[].login] | join(",")' || /usr/bin/echo "")
|
|
19
|
+
|
|
20
|
+
if [[ -n "$ASSIGNEES" ]]; then
|
|
21
|
+
gate_block "issue is assigned to [$ASSIGNEES]" "wait for them to drop it, or pick a different candidate. Override only if you've coordinated with the assignee in a comment."
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
gate_pass "no assignees on issue"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Catalog: A2 — Claim work that's already merged in another PR
|
|
3
|
+
# Mitigates: PostHog #55412 trap (2026-05-02) — issue was open but PR #57145
|
|
4
|
+
# had already merged the principled fix. Without this check we'd have
|
|
5
|
+
# submitted a duplicate and burned an AI policy strike.
|
|
6
|
+
source "$(dirname "$0")/lib/preamble.sh"
|
|
7
|
+
|
|
8
|
+
gate_read_input
|
|
9
|
+
|
|
10
|
+
ISSUE_NUM=$(fm_field "$GATE_CANDIDATE_PATH" "issue_number")
|
|
11
|
+
if [[ -z "$ISSUE_NUM" || -z "$GATE_REPO" ]]; then
|
|
12
|
+
gate_skip "no issue_number or repo in candidate"
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
# Check each of GitHub's three auto-close keywords. NB: gh search uses --merged
|
|
16
|
+
# (NOT --state=merged), and "X OR Y OR Z" is treated as a literal phrase, so
|
|
17
|
+
# we must run separate calls. (Lessons logged in scout's MEMORY.md.)
|
|
18
|
+
SHIPPED_PR=""
|
|
19
|
+
for KW in closes fixes resolves ; do
|
|
20
|
+
HIT=$(gh_safe search prs --repo="$GATE_REPO" "$KW #$ISSUE_NUM" --merged --limit 1 --json url --jq '.[0].url // empty' || /usr/bin/echo "")
|
|
21
|
+
if [[ -n "$HIT" ]]; then
|
|
22
|
+
SHIPPED_PR="$HIT"
|
|
23
|
+
break
|
|
24
|
+
fi
|
|
25
|
+
done
|
|
26
|
+
|
|
27
|
+
if [[ -n "$SHIPPED_PR" ]]; then
|
|
28
|
+
gate_block "issue already shipped in $SHIPPED_PR" "the issue is open but its fix has already merged. Post a 'safe to close?' comment on the issue (good citizen move) and pick a different candidate."
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
gate_pass "no merged PR claims to close this issue"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Catalog: A3 — Issue flagged as duplicate in body or comments
|
|
3
|
+
source "$(dirname "$0")/lib/preamble.sh"
|
|
4
|
+
|
|
5
|
+
gate_read_input
|
|
6
|
+
|
|
7
|
+
ISSUE_NUM=$(fm_field "$GATE_CANDIDATE_PATH" "issue_number")
|
|
8
|
+
if [[ -z "$ISSUE_NUM" || -z "$GATE_REPO" ]]; then
|
|
9
|
+
gate_skip "no issue_number or repo in candidate"
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
TEXT=$(gh_safe issue view "$ISSUE_NUM" --repo "$GATE_REPO" --json body,comments --jq '.body + " " + (.comments | map(.body) | join(" "))' || /usr/bin/echo "")
|
|
13
|
+
|
|
14
|
+
if [[ -z "$TEXT" ]]; then
|
|
15
|
+
gate_inform "could not fetch issue body/comments"
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
if /usr/bin/printf '%s' "$TEXT" | /usr/bin/grep -qiE "(duplicate of|dupe of|see)[[:space:]]+#[0-9]+"; then
|
|
19
|
+
gate_warn "issue body or comments mention being a duplicate" "verify with the maintainer before claiming; the underlying issue may be elsewhere"
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
gate_pass "no duplicate references found"
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Catalog: A4 — Issue is too old (stale, abandoned, or contested)
|
|
3
|
+
# Mitigates: stale issues are a real anti-signal — either the maintainer
|
|
4
|
+
# decided not to fix it, the underlying code changed and the issue's premise
|
|
5
|
+
# no longer holds, or someone has been silently working on it for months
|
|
6
|
+
# without progress. None of those are good for our merge probability.
|
|
7
|
+
#
|
|
8
|
+
# Thresholds (configurable via dossier `max_issue_age_days:`):
|
|
9
|
+
# issue_age <= 90 days → PASS
|
|
10
|
+
# issue_age 91-180 days → WARN ("aging — verify it's still actionable")
|
|
11
|
+
# issue_age 181-365 days → BLOCK ("stale; recommend pick a fresher target")
|
|
12
|
+
# issue_age > 365 days → BLOCK + harder ("zombie issue; fix likely landed elsewhere")
|
|
13
|
+
#
|
|
14
|
+
# A repo with `max_issue_age_days: N` in dossier overrides the default 180.
|
|
15
|
+
source "$(dirname "$0")/lib/preamble.sh"
|
|
16
|
+
|
|
17
|
+
gate_read_input
|
|
18
|
+
|
|
19
|
+
ISSUE_NUM=$(fm_field "$GATE_CANDIDATE_PATH" "issue_number")
|
|
20
|
+
if [[ -z "$ISSUE_NUM" || -z "$GATE_REPO" ]]; then
|
|
21
|
+
gate_skip "no issue_number or repo in candidate"
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
# Read max_issue_age from dossier, default 180
|
|
25
|
+
MAX_AGE_DAYS=180
|
|
26
|
+
if [[ -n "$GATE_DOSSIER_PATH" && -f "$GATE_DOSSIER_PATH" ]]; then
|
|
27
|
+
V=$(fm_field "$GATE_DOSSIER_PATH" "max_issue_age_days")
|
|
28
|
+
[[ -n "$V" && "$V" =~ ^[0-9]+$ ]] && MAX_AGE_DAYS="$V"
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# Live fetch — issue createdAt + last activity (updatedAt + last comment)
|
|
32
|
+
META_JSON=$(gh_safe issue view "$ISSUE_NUM" --repo "$GATE_REPO" --json createdAt,updatedAt,comments --jq '{createdAt, updatedAt, last_comment_at: (.comments | sort_by(.createdAt) | .[-1].createdAt // null)}' || /usr/bin/echo "")
|
|
33
|
+
|
|
34
|
+
if [[ -z "$META_JSON" ]] ; then
|
|
35
|
+
gate_skip "couldn't fetch issue metadata (gh failure?)"
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
CREATED=$(/usr/bin/printf '%s' "$META_JSON" | jq -r '.createdAt // ""')
|
|
39
|
+
UPDATED=$(/usr/bin/printf '%s' "$META_JSON" | jq -r '.updatedAt // ""')
|
|
40
|
+
LAST_COMMENT=$(/usr/bin/printf '%s' "$META_JSON" | jq -r '.last_comment_at // ""')
|
|
41
|
+
|
|
42
|
+
if [[ -z "$CREATED" ]] ; then
|
|
43
|
+
gate_skip "issue createdAt missing in API response"
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
NOW_EPOCH=$(/usr/bin/date -u +%s)
|
|
47
|
+
CREATED_EPOCH=$(/usr/bin/date -u -d "$CREATED" +%s 2>/dev/null || echo 0)
|
|
48
|
+
AGE_DAYS=$(( (NOW_EPOCH - CREATED_EPOCH) / 86400 ))
|
|
49
|
+
|
|
50
|
+
# Activity recency — most recent of updatedAt or last comment
|
|
51
|
+
LATEST_ACT_EPOCH=0
|
|
52
|
+
if [[ -n "$UPDATED" ]] ; then
|
|
53
|
+
V=$(/usr/bin/date -u -d "$UPDATED" +%s 2>/dev/null || echo 0)
|
|
54
|
+
[[ "$V" -gt "$LATEST_ACT_EPOCH" ]] && LATEST_ACT_EPOCH="$V"
|
|
55
|
+
fi
|
|
56
|
+
if [[ -n "$LAST_COMMENT" && "$LAST_COMMENT" != "null" ]] ; then
|
|
57
|
+
V=$(/usr/bin/date -u -d "$LAST_COMMENT" +%s 2>/dev/null || echo 0)
|
|
58
|
+
[[ "$V" -gt "$LATEST_ACT_EPOCH" ]] && LATEST_ACT_EPOCH="$V"
|
|
59
|
+
fi
|
|
60
|
+
SILENCE_DAYS=$(( (NOW_EPOCH - LATEST_ACT_EPOCH) / 86400 ))
|
|
61
|
+
|
|
62
|
+
# Verdicts
|
|
63
|
+
if [[ "$AGE_DAYS" -gt 365 ]] ; then
|
|
64
|
+
gate_block "issue is ${AGE_DAYS}d old (>365d zombie threshold)" "1+ year-old issues are usually either fixed elsewhere, abandoned, or have premises that no longer apply. Pick a fresher target. Override only with a written rationale referencing what's changed."
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
if [[ "$AGE_DAYS" -gt "$MAX_AGE_DAYS" ]] ; then
|
|
68
|
+
gate_block "issue is ${AGE_DAYS}d old (max for this repo: ${MAX_AGE_DAYS}d)" "stale issue. Maintainer engagement likely dropped. Pick a fresher target — or override with rationale if you have direct maintainer signal that it's still wanted."
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# Activity-based check — even fresh issues with long silence are suspect
|
|
72
|
+
if [[ "$SILENCE_DAYS" -gt 90 ]] ; then
|
|
73
|
+
gate_warn "issue is fresh (${AGE_DAYS}d old) but no activity in ${SILENCE_DAYS}d" "comment first to ping for current relevance before investing time"
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
if [[ "$AGE_DAYS" -gt 90 ]] ; then
|
|
77
|
+
gate_warn "issue is ${AGE_DAYS}d old (aging — under the ${MAX_AGE_DAYS}d block threshold but past the 90d sweet spot)" "verify the underlying premise still holds in current code; verify maintainer still wants this fix"
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
gate_pass "issue is ${AGE_DAYS}d old, last activity ${SILENCE_DAYS}d ago"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Catalog: A5 — Issue is still OPEN right now (not closed since dossier built)
|
|
3
|
+
# Mitigates: lingdojo/kana-dojo #15441 trap (2026-05-03) — issue was OPEN at
|
|
4
|
+
# 04:54Z when scout shortlisted it; CLOSED at 05:00Z (NOT_PLANNED) by
|
|
5
|
+
# maintainer killing a bot-generated cron issue. Our dossier and the existing
|
|
6
|
+
# A1/A2 gates wouldn't have caught it because they check assignees and
|
|
7
|
+
# already-shipped, not state. This is the simplest gate to write and the
|
|
8
|
+
# highest-value catch — bot-generated issues at active repos can close
|
|
9
|
+
# minutes after discovery.
|
|
10
|
+
source "$(dirname "$0")/lib/preamble.sh"
|
|
11
|
+
|
|
12
|
+
gate_read_input
|
|
13
|
+
|
|
14
|
+
ISSUE_NUM=$(fm_field "$GATE_CANDIDATE_PATH" "issue_number")
|
|
15
|
+
if [[ -z "$ISSUE_NUM" || -z "$GATE_REPO" ]]; then
|
|
16
|
+
gate_skip "no issue_number or repo in candidate"
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
STATE_JSON=$(gh_safe issue view "$ISSUE_NUM" --repo "$GATE_REPO" --json state,stateReason,closedAt --jq '{state, stateReason: (.stateReason // ""), closedAt: (.closedAt // "")}' || /usr/bin/echo "")
|
|
20
|
+
|
|
21
|
+
if [[ -z "$STATE_JSON" ]] ; then
|
|
22
|
+
gate_skip "couldn't fetch issue state"
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
STATE=$(/usr/bin/printf '%s' "$STATE_JSON" | jq -r '.state')
|
|
26
|
+
REASON=$(/usr/bin/printf '%s' "$STATE_JSON" | jq -r '.stateReason')
|
|
27
|
+
CLOSED_AT=$(/usr/bin/printf '%s' "$STATE_JSON" | jq -r '.closedAt' | /usr/bin/cut -c1-19)
|
|
28
|
+
|
|
29
|
+
if [[ "$STATE" == "CLOSED" ]] ; then
|
|
30
|
+
gate_block "issue is CLOSED (reason: ${REASON:-unspecified}, closed at ${CLOSED_AT:-unknown})" "issue closed since shortlist. If reason is COMPLETED, fix already shipped — pick a different target. If NOT_PLANNED, maintainer rejected the work — drop the candidate."
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
gate_pass "issue state = OPEN"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Catalog: A6 — Repo requires claim-etiquette comment before working
|
|
3
|
+
source "$(dirname "$0")/lib/preamble.sh"
|
|
4
|
+
|
|
5
|
+
gate_read_input
|
|
6
|
+
|
|
7
|
+
if [[ -z "$GATE_DOSSIER_PATH" || ! -f "$GATE_DOSSIER_PATH" ]]; then
|
|
8
|
+
gate_skip "no dossier"
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
REQUIRED=$(fm_field "$GATE_DOSSIER_PATH" "etiquette_comment_required")
|
|
12
|
+
if [[ "$REQUIRED" != "true" ]]; then
|
|
13
|
+
gate_skip "etiquette_comment_required is not true"
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
ISSUE_NUM=$(fm_field "$GATE_CANDIDATE_PATH" "issue_number")
|
|
17
|
+
if [[ -z "$ISSUE_NUM" || -z "$GATE_REPO" ]]; then
|
|
18
|
+
gate_skip "no issue_number or repo in candidate"
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
LOGIN=$(gh_safe api user --jq .login || /usr/bin/echo "")
|
|
22
|
+
if [[ -z "$LOGIN" ]]; then
|
|
23
|
+
gate_inform "could not resolve gh user login"
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
COMMENTS=$(gh_safe issue view "$ISSUE_NUM" --repo "$GATE_REPO" --json comments --jq "[.comments[] | select(.author.login == \"$LOGIN\") | .body] | join(\"\n---\n\")" || /usr/bin/echo "")
|
|
27
|
+
|
|
28
|
+
if [[ -z "$COMMENTS" ]]; then
|
|
29
|
+
gate_block "no claim comment from $LOGIN found on issue #$ISSUE_NUM" "this repo wants you to comment on the issue before working on it; post a claim comment and re-run the transition"
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
if /usr/bin/printf '%s' "$COMMENTS" | /usr/bin/grep -qiE "(i'?d like to take this|i'?ll take this|happy to take this|working on this|claiming this|would like to work on|let me know if i can take|i'?d be happy to work)"; then
|
|
33
|
+
gate_pass "found claim-shaped comment from $LOGIN"
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
gate_block "no claim-shaped comment from $LOGIN on issue #$ISSUE_NUM" "this repo wants you to comment on the issue before working on it; post a claim comment and re-run the transition"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Catalog: A9 — @-mentions in claim/PR text when CODEOWNERS routing exists
|
|
3
|
+
source "$(dirname "$0")/lib/preamble.sh"
|
|
4
|
+
|
|
5
|
+
gate_read_input
|
|
6
|
+
|
|
7
|
+
if [[ -z "$GATE_DOSSIER_PATH" || ! -f "$GATE_DOSSIER_PATH" ]]; then
|
|
8
|
+
gate_skip "no dossier"
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
# Check for CODEOWNERS in dossier — frontmatter policy_files OR Policy file inventory section
|
|
12
|
+
HAS_CODEOWNERS=""
|
|
13
|
+
POLICY_FM=$(fm_field "$GATE_DOSSIER_PATH" "policy_files")
|
|
14
|
+
if /usr/bin/printf '%s' "$POLICY_FM" | /usr/bin/grep -qi "CODEOWNERS"; then
|
|
15
|
+
HAS_CODEOWNERS=1
|
|
16
|
+
fi
|
|
17
|
+
if [[ -z "$HAS_CODEOWNERS" ]]; then
|
|
18
|
+
if /usr/bin/awk '/^## Policy file inventory/{flag=1;next} /^## /{flag=0} flag' "$GATE_DOSSIER_PATH" 2>/dev/null | /usr/bin/grep -qi "\*\*CODEOWNERS\*\*"; then
|
|
19
|
+
HAS_CODEOWNERS=1
|
|
20
|
+
fi
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
if [[ -z "$HAS_CODEOWNERS" ]]; then
|
|
24
|
+
gate_skip "no CODEOWNERS in dossier policy inventory"
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Pick the right draft section based on action
|
|
28
|
+
SECTION=""
|
|
29
|
+
case "$GATE_ACTION" in
|
|
30
|
+
*post-comment*|*claim*) SECTION="## Claim comment draft" ;;
|
|
31
|
+
*open-pr*|*pr*) SECTION="## PR body" ;;
|
|
32
|
+
*) SECTION="## Claim comment draft" ;;
|
|
33
|
+
esac
|
|
34
|
+
|
|
35
|
+
DRAFT=$(/usr/bin/awk -v s="$SECTION" 'BEGIN{flag=0} $0==s{flag=1;next} /^## /{flag=0} flag' "$GATE_CANDIDATE_PATH" 2>/dev/null || /usr/bin/echo "")
|
|
36
|
+
|
|
37
|
+
if [[ -z "$DRAFT" ]]; then
|
|
38
|
+
gate_skip "no draft section found in candidate"
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# Count @-mentions, excluding @me, @everyone, @channel.
|
|
42
|
+
# Use awk (single pass, no pipeline-failure surface) instead of a chained
|
|
43
|
+
# grep | grep | wc — under set -uo pipefail, an empty `grep -oE` returns
|
|
44
|
+
# exit 1, killing the whole pipeline, even though "no mentions" is the
|
|
45
|
+
# correct/expected answer for most drafts.
|
|
46
|
+
MENTIONS=$(/usr/bin/printf '%s' "$DRAFT" | /usr/bin/awk '
|
|
47
|
+
{
|
|
48
|
+
for (i = 1; i <= NF; i++) {
|
|
49
|
+
tok = $i
|
|
50
|
+
gsub(/[,.;:!?)\]"\x27]+$/, "", tok)
|
|
51
|
+
if (tok ~ /^@[a-zA-Z0-9-]{2,}$/ && tok !~ /^@(me|everyone|channel)$/) {
|
|
52
|
+
c++
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
END { print c + 0 }
|
|
57
|
+
')
|
|
58
|
+
|
|
59
|
+
if [[ "${MENTIONS:-0}" -ge 1 ]]; then
|
|
60
|
+
gate_warn "$MENTIONS @-mention(s) in draft despite CODEOWNERS routing" "this repo uses CODEOWNERS; explicit @-mentions are usually noise. Drop them unless you have a specific reason to ping someone."
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
gate_pass "no @-mentions in draft"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Catalog: B1 — Local branch is based on a non-default upstream branch
|
|
3
|
+
source "$(dirname "$0")/lib/preamble.sh"
|
|
4
|
+
|
|
5
|
+
gate_read_input
|
|
6
|
+
|
|
7
|
+
if [[ -z "$GATE_DOSSIER_PATH" || ! -f "$GATE_DOSSIER_PATH" ]]; then
|
|
8
|
+
gate_skip "no dossier — cannot determine default_branch"
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
DEFAULT_BRANCH=$(fm_field "$GATE_DOSSIER_PATH" "default_branch")
|
|
12
|
+
if [[ -z "$DEFAULT_BRANCH" ]]; then
|
|
13
|
+
gate_skip "no default_branch in dossier"
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
REPO_NAME="${GATE_REPO##*/}"
|
|
17
|
+
CLONE_DIR="$HOME/000-projects/contributing-clanker/$REPO_NAME"
|
|
18
|
+
|
|
19
|
+
if [[ ! -d "$CLONE_DIR/.git" ]]; then
|
|
20
|
+
gate_skip "no local clone at $CLONE_DIR"
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
cd "$CLONE_DIR" || gate_skip "cannot cd to clone dir"
|
|
24
|
+
|
|
25
|
+
if /usr/bin/git merge-base --is-ancestor "origin/$DEFAULT_BRANCH" HEAD 2>/dev/null; then
|
|
26
|
+
gate_pass "HEAD descends from origin/$DEFAULT_BRANCH"
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
gate_block "HEAD is not a descendant of origin/$DEFAULT_BRANCH" "rebase onto the default branch: git fetch origin $DEFAULT_BRANCH && git rebase origin/$DEFAULT_BRANCH"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Catalog: B2 — Local branch name violates project's branch_convention regex
|
|
3
|
+
source "$(dirname "$0")/lib/preamble.sh"
|
|
4
|
+
|
|
5
|
+
gate_read_input
|
|
6
|
+
|
|
7
|
+
if [[ -z "$GATE_DOSSIER_PATH" || ! -f "$GATE_DOSSIER_PATH" ]]; then
|
|
8
|
+
gate_skip "no dossier"
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
CONVENTION=$(fm_field "$GATE_DOSSIER_PATH" "branch_convention")
|
|
12
|
+
if [[ -z "$CONVENTION" ]]; then
|
|
13
|
+
gate_skip "no branch_convention in dossier"
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
REPO_NAME="${GATE_REPO##*/}"
|
|
17
|
+
CLONE_DIR="$HOME/000-projects/contributing-clanker/$REPO_NAME"
|
|
18
|
+
|
|
19
|
+
if [[ ! -d "$CLONE_DIR/.git" ]]; then
|
|
20
|
+
gate_skip "no local clone at $CLONE_DIR"
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
cd "$CLONE_DIR" || gate_skip "cannot cd to clone dir"
|
|
24
|
+
|
|
25
|
+
BRANCH=$(/usr/bin/git rev-parse --abbrev-ref HEAD 2>/dev/null || /usr/bin/echo "")
|
|
26
|
+
if [[ -z "$BRANCH" || "$BRANCH" == "HEAD" ]]; then
|
|
27
|
+
gate_skip "detached HEAD or unknown branch"
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
if /usr/bin/printf '%s' "$BRANCH" | /usr/bin/grep -qE "$CONVENTION"; then
|
|
31
|
+
gate_pass "branch '$BRANCH' matches convention"
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
gate_block "branch '$BRANCH' does not match convention /$CONVENTION/" "rename the branch: git branch -m <new-name-matching-convention>"
|