@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,62 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Catalog: C13 — Required review bots haven't reviewed/commented yet
|
|
3
|
+
source "$(dirname "$0")/lib/preamble.sh"
|
|
4
|
+
|
|
5
|
+
gate_read_input
|
|
6
|
+
|
|
7
|
+
PR_NUMBER=$(fm_field "$GATE_CANDIDATE_PATH" "pr_number")
|
|
8
|
+
if [[ -z "$PR_NUMBER" ]]; then
|
|
9
|
+
gate_skip "no pr_number in candidate (no PR yet)"
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
# Parse review_bots: list from dossier — ` - <name>` lines following `review_bots:`
|
|
13
|
+
BOTS_RAW=$(/usr/bin/awk '
|
|
14
|
+
/^---$/ { fm = !fm ? 1 : 2; next }
|
|
15
|
+
fm != 1 { next }
|
|
16
|
+
/^review_bots:/ { collecting = 1; next }
|
|
17
|
+
collecting && /^[[:space:]]+-[[:space:]]+/ {
|
|
18
|
+
sub(/^[[:space:]]+-[[:space:]]+/, "")
|
|
19
|
+
gsub(/^"|"$/, "")
|
|
20
|
+
print
|
|
21
|
+
next
|
|
22
|
+
}
|
|
23
|
+
collecting && /^[A-Za-z]/ { collecting = 0 }
|
|
24
|
+
' "$GATE_DOSSIER_PATH" 2>/dev/null || /usr/bin/echo "")
|
|
25
|
+
|
|
26
|
+
if [[ -z "$BOTS_RAW" ]]; then
|
|
27
|
+
gate_skip "no review_bots listed in dossier"
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# Filter "(none detected)" sentinel
|
|
31
|
+
declare -a BOTS=()
|
|
32
|
+
while IFS= read -r line; do
|
|
33
|
+
[[ -z "$line" ]] && continue
|
|
34
|
+
[[ "$line" == "(none detected)" ]] && continue
|
|
35
|
+
BOTS+=("$line")
|
|
36
|
+
done <<< "$BOTS_RAW"
|
|
37
|
+
|
|
38
|
+
if (( ${#BOTS[@]} == 0 )); then
|
|
39
|
+
gate_skip "review_bots list empty or (none detected)"
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Pull all reviewer + commenter logins from the PR
|
|
43
|
+
REVIEWERS=$(gh_safe pr view "$PR_NUMBER" --repo "$GATE_REPO" --json reviews,comments \
|
|
44
|
+
--jq '[.reviews[].author.login, .comments[].author.login] | unique | join(",")' 2>/dev/null || /usr/bin/echo "")
|
|
45
|
+
|
|
46
|
+
# For each required bot, substring match against the reviewers/commenters list (lowercased)
|
|
47
|
+
REVIEWERS_LC=$(/usr/bin/printf '%s' "$REVIEWERS" | /usr/bin/tr '[:upper:]' '[:lower:]')
|
|
48
|
+
declare -a MISSING=()
|
|
49
|
+
for bot in "${BOTS[@]}"; do
|
|
50
|
+
bot_lc=$(/usr/bin/printf '%s' "$bot" | /usr/bin/tr '[:upper:]' '[:lower:]' | /usr/bin/tr -d '-')
|
|
51
|
+
reviewers_norm=$(/usr/bin/printf '%s' "$REVIEWERS_LC" | /usr/bin/tr -d '-')
|
|
52
|
+
if [[ "$reviewers_norm" != *"$bot_lc"* ]]; then
|
|
53
|
+
MISSING+=("$bot")
|
|
54
|
+
fi
|
|
55
|
+
done
|
|
56
|
+
|
|
57
|
+
if (( ${#MISSING[@]} > 0 )); then
|
|
58
|
+
joined=$(IFS=', '; /usr/bin/printf '%s' "${MISSING[*]}")
|
|
59
|
+
gate_warn "waiting on review from: $joined" "wait for these bots to weigh in before flipping to ready-for-review"
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
gate_pass "all required review bots have engaged on the PR"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Catalog: C16 — author attempts to merge their own PR
|
|
3
|
+
# Mitigates: bypassing maintainer review on a contribution is a hard etiquette violation.
|
|
4
|
+
source "$(dirname "$0")/lib/preamble.sh"
|
|
5
|
+
|
|
6
|
+
gate_read_input
|
|
7
|
+
|
|
8
|
+
PR_NUM=$(fm_field "$GATE_CANDIDATE_PATH" "pr_number")
|
|
9
|
+
if [[ -z "$PR_NUM" || -z "$GATE_REPO" ]]; then
|
|
10
|
+
gate_skip "no pr_number or repo in candidate"
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
PR_AUTHOR=$(gh_safe pr view "$PR_NUM" --repo "$GATE_REPO" --json author --jq '.author.login' || /usr/bin/echo "")
|
|
14
|
+
ME=$(gh_safe api user --jq '.login' || /usr/bin/echo "")
|
|
15
|
+
|
|
16
|
+
if [[ -z "$PR_AUTHOR" || -z "$ME" ]]; then
|
|
17
|
+
gate_skip "could not resolve PR author or current user"
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
if [[ "$PR_AUTHOR" == "$ME" ]]; then
|
|
21
|
+
gate_block "you ($ME) authored PR #$PR_NUM — self-merge would bypass maintainer review" "you authored this PR — wait for a maintainer to merge"
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
gate_pass "PR author ($PR_AUTHOR) differs from current user ($ME)"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Catalog: C19 — PR body claims that don't match the diff
|
|
3
|
+
# Mitigates: Round-1 maintainer GAP-6 — close-on-sight pattern: PR body
|
|
4
|
+
# claims "added tests" / "updated CHANGELOG" / "added migration notes" but
|
|
5
|
+
# `git diff` shows no corresponding files touched. Pure AI-confabulation.
|
|
6
|
+
# Fastest path from "AI-assisted" to "AI-fabricated" in maintainer's eye.
|
|
7
|
+
source "$(dirname "$0")/lib/preamble.sh"
|
|
8
|
+
|
|
9
|
+
gate_read_input
|
|
10
|
+
|
|
11
|
+
# Need both PR body and a way to inspect the diff
|
|
12
|
+
PR_BODY=$(/usr/bin/awk '/^## PR body/{flag=1;next} /^## /{flag=0} flag' "$GATE_CANDIDATE_PATH" 2>/dev/null || /usr/bin/echo "")
|
|
13
|
+
LOCAL_CLONE=$(fm_field "$GATE_CANDIDATE_PATH" "local_clone_path")
|
|
14
|
+
BRANCH=$(fm_field "$GATE_CANDIDATE_PATH" "branch")
|
|
15
|
+
|
|
16
|
+
if [[ -z "$PR_BODY" ]]; then
|
|
17
|
+
gate_inform "no PR body drafted yet"
|
|
18
|
+
fi
|
|
19
|
+
if [[ -z "$LOCAL_CLONE" || ! -d "$LOCAL_CLONE" ]]; then
|
|
20
|
+
gate_skip "no local_clone_path; cannot inspect diff"
|
|
21
|
+
fi
|
|
22
|
+
if [[ -z "$BRANCH" ]]; then
|
|
23
|
+
gate_skip "no branch in candidate"
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# Get the list of changed files vs default branch (heuristic: master/main)
|
|
27
|
+
DEFAULT_BRANCH="main"
|
|
28
|
+
if [[ -n "$GATE_DOSSIER_PATH" && -f "$GATE_DOSSIER_PATH" ]]; then
|
|
29
|
+
V=$(fm_field "$GATE_DOSSIER_PATH" "default_branch")
|
|
30
|
+
[[ -n "$V" ]] && DEFAULT_BRANCH="$V"
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
DIFF_FILES=$(cd "$LOCAL_CLONE" 2>/dev/null && git diff --name-only "origin/$DEFAULT_BRANCH..$BRANCH" 2>/dev/null || /usr/bin/echo "")
|
|
34
|
+
if [[ -z "$DIFF_FILES" ]]; then
|
|
35
|
+
gate_skip "no diff against origin/$DEFAULT_BRANCH (or git command failed)"
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# Claim → expected-path patterns. Each entry: regex_in_body | regex_required_in_diff
|
|
39
|
+
declare -a CLAIMS=(
|
|
40
|
+
"(added|wrote|new) tests?\b|added test (coverage|cases)|test cases? added|regression test|test for the (bug|fix)|tests?[[:space:]]*passing:[[:space:]]+(yes|added)|cargo test|pytest|jest|mocha|vitest=tests?/"
|
|
41
|
+
"updated? (the )?CHANGELOG=CHANGELOG"
|
|
42
|
+
"(added|updated) (the )?(migration|migration notes|migrations)=(migration|migrations)/"
|
|
43
|
+
"updated? (the )?(README|docs|documentation)=(README|docs/|.md$)"
|
|
44
|
+
"added? (the )?changeset=\.changeset/"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
ISSUES=()
|
|
48
|
+
for ENTRY in "${CLAIMS[@]}"; do
|
|
49
|
+
CLAIM_REGEX="${ENTRY%%=*}"
|
|
50
|
+
PATH_REGEX="${ENTRY##*=}"
|
|
51
|
+
if /usr/bin/printf '%s' "$PR_BODY" | /usr/bin/grep -qiE "$CLAIM_REGEX"; then
|
|
52
|
+
if ! /usr/bin/printf '%s' "$DIFF_FILES" | /usr/bin/grep -qiE "$PATH_REGEX"; then
|
|
53
|
+
MATCHED=$(/usr/bin/printf '%s' "$PR_BODY" | /usr/bin/grep -ioE "$CLAIM_REGEX" | /usr/bin/head -1)
|
|
54
|
+
ISSUES+=("body claims '$MATCHED' but diff doesn't touch $PATH_REGEX")
|
|
55
|
+
fi
|
|
56
|
+
fi
|
|
57
|
+
done
|
|
58
|
+
|
|
59
|
+
if [[ "${#ISSUES[@]}" -gt 0 ]]; then
|
|
60
|
+
REASONS=$(/usr/bin/printf '%s; ' "${ISSUES[@]}")
|
|
61
|
+
gate_block "PR body makes claims not backed by the diff: $REASONS" "either add the work to match the claim, OR remove the claim from the PR body. False claims = closure regardless of code quality (PostHog AI_POLICY: 'PRs that clearly weren't run or tested will be closed')."
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
gate_pass "PR body claims are consistent with diff"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Catalog: D2 — Block AI-shaped issue body when opening a new issue
|
|
3
|
+
# Many repos auto-close machine-generated bug reports on sight. We refuse
|
|
4
|
+
# to ship one in our name.
|
|
5
|
+
source "$(dirname "$0")/lib/preamble.sh"
|
|
6
|
+
|
|
7
|
+
gate_read_input
|
|
8
|
+
|
|
9
|
+
# Extract the "## Issue body draft" section from the candidate
|
|
10
|
+
DRAFT=$(/usr/bin/awk '/^## Issue body draft/{flag=1;next} /^## /{flag=0} flag' "$GATE_CANDIDATE_PATH" 2>/dev/null || /usr/bin/echo "")
|
|
11
|
+
|
|
12
|
+
if [[ -z "${DRAFT// /}" ]]; then
|
|
13
|
+
gate_skip "no issue draft yet"
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
# AI-shaped patterns. Case-insensitive. Each entry is one distinct pattern.
|
|
17
|
+
PATTERNS=(
|
|
18
|
+
"I noticed"
|
|
19
|
+
"It appears"
|
|
20
|
+
"I observed"
|
|
21
|
+
"the AI suggests"
|
|
22
|
+
"Claude suggests"
|
|
23
|
+
"ChatGPT suggests"
|
|
24
|
+
"based on my analysis"
|
|
25
|
+
"after analyzing"
|
|
26
|
+
"I've identified"
|
|
27
|
+
"the issue stems from"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
HITS=()
|
|
31
|
+
for pat in "${PATTERNS[@]}"; do
|
|
32
|
+
if /usr/bin/printf '%s' "$DRAFT" | /usr/bin/grep -qiE "$pat"; then
|
|
33
|
+
HITS+=("$pat")
|
|
34
|
+
fi
|
|
35
|
+
done
|
|
36
|
+
|
|
37
|
+
COUNT=${#HITS[@]}
|
|
38
|
+
JOINED=$(/usr/bin/printf '%s, ' "${HITS[@]}" 2>/dev/null | /usr/bin/sed 's/, $//')
|
|
39
|
+
|
|
40
|
+
if (( COUNT >= 2 )); then
|
|
41
|
+
gate_block "issue draft contains $COUNT AI-shaped phrases: $JOINED" "rewrite the issue in your own voice; many repos auto-close AI-shaped bug reports"
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
if (( COUNT == 1 )); then
|
|
45
|
+
gate_warn "issue draft contains AI-shaped phrase: $JOINED" "consider rewriting in your own voice"
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
gate_pass "no AI-shaped phrases detected"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Catalog: D3 — Block AI-generated review comments on someone else's PR
|
|
3
|
+
# AI-shaped review comments are unwelcome at most repos. Stricter than D2:
|
|
4
|
+
# any single hit blocks.
|
|
5
|
+
source "$(dirname "$0")/lib/preamble.sh"
|
|
6
|
+
|
|
7
|
+
gate_read_input
|
|
8
|
+
|
|
9
|
+
DRAFT=$(/usr/bin/awk '/^## Review draft/{flag=1;next} /^## /{flag=0} flag' "$GATE_CANDIDATE_PATH" 2>/dev/null || /usr/bin/echo "")
|
|
10
|
+
|
|
11
|
+
if [[ -z "${DRAFT// /}" ]]; then
|
|
12
|
+
gate_skip "no review draft yet"
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
PATTERNS=(
|
|
16
|
+
"I noticed"
|
|
17
|
+
"It appears"
|
|
18
|
+
"I observed"
|
|
19
|
+
"the AI suggests"
|
|
20
|
+
"Claude suggests"
|
|
21
|
+
"ChatGPT suggests"
|
|
22
|
+
"based on my analysis"
|
|
23
|
+
"after analyzing"
|
|
24
|
+
"I've identified"
|
|
25
|
+
"the issue stems from"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
HITS=()
|
|
29
|
+
for pat in "${PATTERNS[@]}"; do
|
|
30
|
+
if /usr/bin/printf '%s' "$DRAFT" | /usr/bin/grep -qiE "$pat"; then
|
|
31
|
+
HITS+=("$pat")
|
|
32
|
+
fi
|
|
33
|
+
done
|
|
34
|
+
|
|
35
|
+
COUNT=${#HITS[@]}
|
|
36
|
+
JOINED=$(/usr/bin/printf '%s, ' "${HITS[@]}" 2>/dev/null | /usr/bin/sed 's/, $//')
|
|
37
|
+
|
|
38
|
+
if (( COUNT >= 1 )); then
|
|
39
|
+
gate_block "review draft contains $COUNT AI-shaped phrase(s): $JOINED" "AI-generated review comments are unwelcome at most repos; either rewrite in your own voice or skip the review"
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
gate_pass "no AI-shaped phrases detected"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Catalog: D5 — reopening a PR that a maintainer closed
|
|
3
|
+
# Mitigates: comes across as ignoring maintainer feedback; usually a fresh PR is better.
|
|
4
|
+
source "$(dirname "$0")/lib/preamble.sh"
|
|
5
|
+
|
|
6
|
+
gate_read_input
|
|
7
|
+
|
|
8
|
+
PR_NUM=$(fm_field "$GATE_CANDIDATE_PATH" "pr_number")
|
|
9
|
+
if [[ -z "$PR_NUM" || -z "$GATE_REPO" ]]; then
|
|
10
|
+
gate_skip "no pr_number or repo in candidate"
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
INFO=$(gh_safe pr view "$PR_NUM" --repo "$GATE_REPO" --json state,closedAt --jq '{state: .state, closedAt: .closedAt}' || /usr/bin/echo "")
|
|
14
|
+
if [[ -z "$INFO" ]]; then
|
|
15
|
+
gate_skip "could not fetch PR state"
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
STATE=$(/usr/bin/printf '%s' "$INFO" | jq -r '.state // ""')
|
|
19
|
+
CLOSED_AT=$(/usr/bin/printf '%s' "$INFO" | jq -r '.closedAt // ""')
|
|
20
|
+
|
|
21
|
+
if [[ "$STATE" != "CLOSED" || -z "$CLOSED_AT" || "$CLOSED_AT" == "null" ]]; then
|
|
22
|
+
gate_skip "PR is not in CLOSED state (state=$STATE) — nothing to reopen"
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
gate_warn "PR #$PR_NUM was closed at $CLOSED_AT — reopening may be read as ignoring the maintainer" "the maintainer closed this PR; reopening without addressing their feedback can come across as disrespectful. Consider a fresh PR if you've made substantial changes"
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Catalog: E2 — AI policy strike tracking, ORG-LEVEL (not just repo-level)
|
|
3
|
+
# Mitigates: Round-1 AI-policy GAP-6 — PostHog AI_POLICY says "Two or more
|
|
4
|
+
# closures: We'll block the account." Stake is account-level, not per-repo.
|
|
5
|
+
# A 1st closure on PostHog/posthog + 1st on PostHog/posthog-js = blocked,
|
|
6
|
+
# and a per-repo gate would never see it. This gate evaluates strikes at
|
|
7
|
+
# the org-owner level.
|
|
8
|
+
source "$(dirname "$0")/lib/preamble.sh"
|
|
9
|
+
|
|
10
|
+
gate_read_input
|
|
11
|
+
|
|
12
|
+
if [[ -z "$GATE_REPO" ]]; then
|
|
13
|
+
gate_skip "no repo in candidate"
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
# Read strike scope from dossier (default: org)
|
|
17
|
+
STRIKE_SCOPE="org"
|
|
18
|
+
if [[ -n "$GATE_DOSSIER_PATH" && -f "$GATE_DOSSIER_PATH" ]]; then
|
|
19
|
+
V=$(fm_field "$GATE_DOSSIER_PATH" "strike_scope")
|
|
20
|
+
[[ -n "$V" ]] && STRIKE_SCOPE="$V"
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
OWNER="${GATE_REPO%%/*}"
|
|
24
|
+
LOG="$HOME/.contribute-system/log.jsonl"
|
|
25
|
+
|
|
26
|
+
if [[ ! -f "$LOG" ]]; then
|
|
27
|
+
gate_pass "no log.jsonl yet (no prior strikes possible)"
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# Count prior closures with reason matching AI policy at the appropriate scope.
|
|
31
|
+
# Reason patterns: any 'dropped' event with reason containing 'ai_policy', 'ai-policy', 'slop', 'ai policy'
|
|
32
|
+
case "$STRIKE_SCOPE" in
|
|
33
|
+
repo) SCOPE_FILTER=".details.repo == \"$GATE_REPO\"" ;;
|
|
34
|
+
org) SCOPE_FILTER=".details.repo | startswith(\"$OWNER/\")" ;;
|
|
35
|
+
account) SCOPE_FILTER="true" ;;
|
|
36
|
+
*) gate_block "unknown strike_scope in dossier: $STRIKE_SCOPE" "set strike_scope to repo|org|account in the dossier" ;;
|
|
37
|
+
esac
|
|
38
|
+
|
|
39
|
+
STRIKE_COUNT=$(jq -c "
|
|
40
|
+
select(.event == \"candidate_dropped\")
|
|
41
|
+
| select($SCOPE_FILTER)
|
|
42
|
+
| select(.details.reason | tostring | test(\"ai[ _-]?policy|ai[ _-]?slop\"; \"i\"))
|
|
43
|
+
" "$LOG" 2>/dev/null | /usr/bin/wc -l | /usr/bin/awk '{print $1}')
|
|
44
|
+
|
|
45
|
+
# Note: pre-Phase-3, there's no way to query PostHog's ACTUAL record of our
|
|
46
|
+
# strikes; we only know what WE logged. If we never logged a closure (e.g.,
|
|
47
|
+
# PostHog closed without us calling it out), we'd undercount. Best-effort.
|
|
48
|
+
|
|
49
|
+
if [[ "${STRIKE_COUNT:-0}" -ge 1 ]]; then
|
|
50
|
+
if [[ "$STRIKE_COUNT" -eq 1 ]]; then
|
|
51
|
+
gate_block "1 prior AI-policy closure at $STRIKE_SCOPE-scope ($OWNER). Next closure = account block at PostHog-tier repos." "manual override required: --override-gate=E2 \"<reason you're confident this PR won't get closed>\". This is the gate that prevents account-block."
|
|
52
|
+
else
|
|
53
|
+
gate_block "$STRIKE_COUNT prior AI-policy closures at $STRIKE_SCOPE-scope ($OWNER). HARD STOP — pause contributions to this org until you've discussed with maintainers." "manual override is intentionally inconvenient here. If you must proceed, --override-gate=E2 \"<written rationale>\" AND post a comment on the issue acknowledging the prior closures."
|
|
54
|
+
fi
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
gate_pass "no prior AI-policy closures at $STRIKE_SCOPE-scope ($OWNER)"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Catalog: E4 — Verify origin points at user's fork (not upstream)
|
|
3
|
+
# Mitigates: pushing to someone else's repo by accident, or being unable to
|
|
4
|
+
# push at all.
|
|
5
|
+
source "$(dirname "$0")/lib/preamble.sh"
|
|
6
|
+
|
|
7
|
+
gate_read_input
|
|
8
|
+
|
|
9
|
+
CLONE="$HOME/000-projects/contributing-clanker/${GATE_REPO##*/}"
|
|
10
|
+
|
|
11
|
+
if [[ ! -d "$CLONE/.git" ]]; then
|
|
12
|
+
gate_skip "no local clone at $CLONE"
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
USER_LOGIN=$(gh_safe api user --jq .login 2>/dev/null || /usr/bin/echo "")
|
|
16
|
+
if [[ -z "$USER_LOGIN" ]]; then
|
|
17
|
+
gate_skip "could not resolve current gh user login"
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
ORIGIN_URL=$(/usr/bin/git -C "$CLONE" remote get-url origin 2>/dev/null || /usr/bin/echo "")
|
|
21
|
+
if [[ -z "$ORIGIN_URL" ]]; then
|
|
22
|
+
gate_block "no origin remote configured in $CLONE" "set origin to your fork: gh repo fork ${GATE_REPO} --remote --remote-name origin"
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# Parse owner from URL — handles SSH (git@github.com:owner/repo.git) and
|
|
26
|
+
# HTTPS (https://github.com/owner/repo.git) forms.
|
|
27
|
+
ORIGIN_OWNER=$(/usr/bin/printf '%s' "$ORIGIN_URL" | /usr/bin/sed -E 's#^git@github\.com:([^/]+)/.*$#\1#; s#^https?://github\.com/([^/]+)/.*$#\1#')
|
|
28
|
+
|
|
29
|
+
UPSTREAM_OWNER="${GATE_REPO%%/*}"
|
|
30
|
+
|
|
31
|
+
if [[ "$ORIGIN_OWNER" == "$UPSTREAM_OWNER" ]]; then
|
|
32
|
+
gate_inform "origin points at upstream ($UPSTREAM_OWNER); pushing directly to upstream — verify you have access"
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
if [[ "$ORIGIN_OWNER" != "$USER_LOGIN" ]]; then
|
|
36
|
+
gate_block "origin owner is '$ORIGIN_OWNER' but your gh login is '$USER_LOGIN'" "set origin to your fork: gh repo fork ${GATE_REPO} --remote --remote-name origin"
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
gate_pass "origin points at your fork ($USER_LOGIN/${GATE_REPO##*/})"
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Catalog: F1 — License compatibility check for newly added dependencies
|
|
3
|
+
# Lists new deps and prompts manual license verification. Does NOT look up
|
|
4
|
+
# SPDX from registries (too heavy for a gate); informational only.
|
|
5
|
+
source "$(dirname "$0")/lib/preamble.sh"
|
|
6
|
+
|
|
7
|
+
gate_read_input
|
|
8
|
+
|
|
9
|
+
CLONE="$HOME/000-projects/contributing-clanker/${GATE_REPO##*/}"
|
|
10
|
+
|
|
11
|
+
if [[ ! -d "$CLONE/.git" ]]; then
|
|
12
|
+
gate_skip "no local clone at $CLONE"
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
DEFAULT_BRANCH=$(fm_field "$GATE_DOSSIER_PATH" "default_branch")
|
|
16
|
+
if [[ -z "$DEFAULT_BRANCH" ]]; then
|
|
17
|
+
gate_skip "no default_branch in dossier"
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
REPO_LICENSE=$(fm_field "$GATE_DOSSIER_PATH" "license")
|
|
21
|
+
[[ -z "$REPO_LICENSE" ]] && REPO_LICENSE="unknown"
|
|
22
|
+
|
|
23
|
+
CHANGED=$(/usr/bin/git -C "$CLONE" diff "$DEFAULT_BRANCH..HEAD" --name-only 2>/dev/null || /usr/bin/echo "")
|
|
24
|
+
|
|
25
|
+
MANIFESTS=()
|
|
26
|
+
while IFS= read -r f; do
|
|
27
|
+
case "$f" in
|
|
28
|
+
package.json|*/package.json) MANIFESTS+=("$f") ;;
|
|
29
|
+
Cargo.toml|*/Cargo.toml) MANIFESTS+=("$f") ;;
|
|
30
|
+
requirements.txt|*/requirements.txt) MANIFESTS+=("$f") ;;
|
|
31
|
+
pyproject.toml|*/pyproject.toml) MANIFESTS+=("$f") ;;
|
|
32
|
+
go.mod|*/go.mod) MANIFESTS+=("$f") ;;
|
|
33
|
+
Gemfile|*/Gemfile) MANIFESTS+=("$f") ;;
|
|
34
|
+
esac
|
|
35
|
+
done <<< "$CHANGED"
|
|
36
|
+
|
|
37
|
+
if (( ${#MANIFESTS[@]} == 0 )); then
|
|
38
|
+
gate_pass "no dependency manifest changes"
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
NEW_DEPS=()
|
|
42
|
+
for m in "${MANIFESTS[@]}"; do
|
|
43
|
+
DIFF=$(/usr/bin/git -C "$CLONE" diff "$DEFAULT_BRANCH..HEAD" -- "$m" 2>/dev/null || /usr/bin/echo "")
|
|
44
|
+
# Best-effort regex per manifest type — extract package names from + lines
|
|
45
|
+
case "$m" in
|
|
46
|
+
*package.json)
|
|
47
|
+
while IFS= read -r line; do
|
|
48
|
+
name=$(/usr/bin/printf '%s' "$line" | /usr/bin/sed -nE 's/^\+[[:space:]]*"([^"]+)":[[:space:]]*"[^"]+".*$/\1/p')
|
|
49
|
+
[[ -n "$name" ]] && NEW_DEPS+=("$name")
|
|
50
|
+
done <<< "$DIFF"
|
|
51
|
+
;;
|
|
52
|
+
*Cargo.toml)
|
|
53
|
+
while IFS= read -r line; do
|
|
54
|
+
name=$(/usr/bin/printf '%s' "$line" | /usr/bin/sed -nE 's/^\+[[:space:]]*([a-zA-Z0-9_-]+)[[:space:]]*=.*$/\1/p')
|
|
55
|
+
[[ -n "$name" ]] && NEW_DEPS+=("$name")
|
|
56
|
+
done <<< "$DIFF"
|
|
57
|
+
;;
|
|
58
|
+
*requirements.txt)
|
|
59
|
+
while IFS= read -r line; do
|
|
60
|
+
name=$(/usr/bin/printf '%s' "$line" | /usr/bin/sed -nE 's/^\+[[:space:]]*([a-zA-Z0-9_.-]+)[[:space:]]*[<>=~!].*$/\1/p; s/^\+[[:space:]]*([a-zA-Z0-9_.-]+)[[:space:]]*$/\1/p')
|
|
61
|
+
[[ -n "$name" ]] && NEW_DEPS+=("$name")
|
|
62
|
+
done <<< "$DIFF"
|
|
63
|
+
;;
|
|
64
|
+
*pyproject.toml)
|
|
65
|
+
while IFS= read -r line; do
|
|
66
|
+
name=$(/usr/bin/printf '%s' "$line" | /usr/bin/sed -nE 's/^\+[[:space:]]*"([a-zA-Z0-9_.-]+)[[:space:]<>=~!].*"$/\1/p; s/^\+[[:space:]]*([a-zA-Z0-9_.-]+)[[:space:]]*=[[:space:]]*".*$/\1/p')
|
|
67
|
+
[[ -n "$name" ]] && NEW_DEPS+=("$name")
|
|
68
|
+
done <<< "$DIFF"
|
|
69
|
+
;;
|
|
70
|
+
*go.mod)
|
|
71
|
+
while IFS= read -r line; do
|
|
72
|
+
name=$(/usr/bin/printf '%s' "$line" | /usr/bin/sed -nE 's/^\+[[:space:]]*([a-zA-Z0-9_./-]+)[[:space:]]+v[0-9].*$/\1/p')
|
|
73
|
+
[[ -n "$name" ]] && NEW_DEPS+=("$name")
|
|
74
|
+
done <<< "$DIFF"
|
|
75
|
+
;;
|
|
76
|
+
*Gemfile)
|
|
77
|
+
while IFS= read -r line; do
|
|
78
|
+
name=$(/usr/bin/printf '%s' "$line" | /usr/bin/sed -nE "s/^\+[[:space:]]*gem[[:space:]]+['\"]([^'\"]+)['\"].*$/\1/p")
|
|
79
|
+
[[ -n "$name" ]] && NEW_DEPS+=("$name")
|
|
80
|
+
done <<< "$DIFF"
|
|
81
|
+
;;
|
|
82
|
+
esac
|
|
83
|
+
done
|
|
84
|
+
|
|
85
|
+
if (( ${#NEW_DEPS[@]} == 0 )); then
|
|
86
|
+
gate_pass "no new dependency entries detected in changed manifests"
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
# Dedupe
|
|
90
|
+
UNIQ=$(/usr/bin/printf '%s\n' "${NEW_DEPS[@]}" | /usr/bin/awk '!seen[$0]++' | /usr/bin/tr '\n' ',' | /usr/bin/sed 's/,$//')
|
|
91
|
+
|
|
92
|
+
gate_inform "new dependencies added: $UNIQ; verify license compatibility with repo's $REPO_LICENSE license manually"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Catalog: F3 — new fixture / sample files that may contain copyrighted external content
|
|
3
|
+
# Mitigates: copying real-world web/user content into fixtures triggers license takedowns.
|
|
4
|
+
source "$(dirname "$0")/lib/preamble.sh"
|
|
5
|
+
|
|
6
|
+
gate_read_input
|
|
7
|
+
|
|
8
|
+
REPO_NAME="${GATE_REPO##*/}"
|
|
9
|
+
CLONE="$HOME/000-projects/contributing-clanker/$REPO_NAME"
|
|
10
|
+
|
|
11
|
+
if [[ ! -d "$CLONE/.git" ]]; then
|
|
12
|
+
gate_skip "no local clone at $CLONE"
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
DEFAULT_BRANCH=""
|
|
16
|
+
if [[ -n "$GATE_DOSSIER_PATH" && -f "$GATE_DOSSIER_PATH" ]]; then
|
|
17
|
+
DEFAULT_BRANCH=$(fm_field "$GATE_DOSSIER_PATH" "default_branch")
|
|
18
|
+
fi
|
|
19
|
+
[[ -z "$DEFAULT_BRANCH" ]] && DEFAULT_BRANCH="main"
|
|
20
|
+
|
|
21
|
+
ADDED=$(/usr/bin/git -C "$CLONE" diff "$DEFAULT_BRANCH..HEAD" --name-only --diff-filter=A 2>/dev/null || /usr/bin/echo "")
|
|
22
|
+
|
|
23
|
+
FIXTURES=$(/usr/bin/printf '%s\n' "$ADDED" | /usr/bin/grep -E '(tests/fixtures/|tests/data/|__fixtures__/|(^|/)fixtures/|test_data/|testdata/|(^|/)samples/)' || /usr/bin/echo "")
|
|
24
|
+
|
|
25
|
+
if [[ -z "$FIXTURES" ]]; then
|
|
26
|
+
gate_pass "no new fixture / sample files added"
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
LIST=$(/usr/bin/printf '%s' "$FIXTURES" | /usr/bin/tr '\n' ',' | /usr/bin/sed 's/,$//')
|
|
30
|
+
gate_inform "new fixture / sample files added — verify provenance manually: $LIST"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Catalog: F4 — Override disclosure required in PR body
|
|
3
|
+
# Mitigates: Round-1 maintainer GAP-10 + security GAP-2 + portfolio GAP-3 —
|
|
4
|
+
# `--override-gate=<id>` writes audit trail to log.jsonl + candidate .md, but
|
|
5
|
+
# both are private to the contributor. From the maintainer's POV, the
|
|
6
|
+
# override mechanism is plausible-deniability theater. This gate forces any
|
|
7
|
+
# PR submitted with overrides to include a `## Safety override disclosure`
|
|
8
|
+
# section in the PR body listing each override + reason.
|
|
9
|
+
source "$(dirname "$0")/lib/preamble.sh"
|
|
10
|
+
|
|
11
|
+
gate_read_input
|
|
12
|
+
|
|
13
|
+
# Read overrides from candidate frontmatter
|
|
14
|
+
OVERRIDES=$(/usr/bin/awk '/^overrides:/{flag=1;next} /^[a-z_]+:/{flag=0} flag' "$GATE_CANDIDATE_PATH" 2>/dev/null || /usr/bin/echo "")
|
|
15
|
+
|
|
16
|
+
if [[ -z "$OVERRIDES" ]]; then
|
|
17
|
+
gate_pass "no overrides used; nothing to disclose"
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# Count overrides
|
|
21
|
+
OVERRIDE_COUNT=$(/usr/bin/printf '%s\n' "$OVERRIDES" | /usr/bin/grep -c 'gate:' || /usr/bin/echo 0)
|
|
22
|
+
|
|
23
|
+
# Read PR body draft from candidate (## PR body section)
|
|
24
|
+
PR_BODY=$(/usr/bin/awk '/^## PR body/{flag=1;next} /^## /{flag=0} flag' "$GATE_CANDIDATE_PATH" 2>/dev/null || /usr/bin/echo "")
|
|
25
|
+
|
|
26
|
+
if [[ -z "$PR_BODY" ]]; then
|
|
27
|
+
gate_inform "$OVERRIDE_COUNT overrides used but no PR body drafted yet — re-run at open-pr"
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# Look for the disclosure section in the PR body
|
|
31
|
+
if /usr/bin/printf '%s' "$PR_BODY" | /usr/bin/grep -qiE '^## (Safety override|Override) disclosure'; then
|
|
32
|
+
# Check that each override gate ID is mentioned in the body
|
|
33
|
+
MISSING=()
|
|
34
|
+
while IFS= read -r OG; do
|
|
35
|
+
GID=$(/usr/bin/printf '%s' "$OG" | /usr/bin/grep -oP 'gate:\s*\K[A-Z0-9]+')
|
|
36
|
+
[[ -z "$GID" ]] && continue
|
|
37
|
+
if ! /usr/bin/printf '%s' "$PR_BODY" | /usr/bin/grep -qE "\b$GID\b"; then
|
|
38
|
+
MISSING+=("$GID")
|
|
39
|
+
fi
|
|
40
|
+
done < <(/usr/bin/printf '%s\n' "$OVERRIDES" | /usr/bin/grep 'gate:')
|
|
41
|
+
|
|
42
|
+
if [[ "${#MISSING[@]}" -eq 0 ]]; then
|
|
43
|
+
gate_pass "all $OVERRIDE_COUNT overrides disclosed in PR body"
|
|
44
|
+
else
|
|
45
|
+
gate_block "PR body has '## Safety override disclosure' section but doesn't mention overrides: ${MISSING[*]}" "list each override gate ID in the disclosure section with the reason you used it"
|
|
46
|
+
fi
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
gate_block "$OVERRIDE_COUNT safety overrides used but PR body lacks '## Safety override disclosure' section" "add a section to the PR body enumerating each override + reason. This is the maintainer-visible audit trail; without it the override mechanism is invisible to the people the safety system is supposed to protect."
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Catalog: G1 — Block hand-edits to vendored / generated paths
|
|
3
|
+
# Vendored dirs (vendor/, node_modules/, dist/, etc.) must regenerate from
|
|
4
|
+
# source. Lockfile changes are common and legitimate (dependency bumps);
|
|
5
|
+
# treat them as WARN, not BLOCK.
|
|
6
|
+
source "$(dirname "$0")/lib/preamble.sh"
|
|
7
|
+
|
|
8
|
+
gate_read_input
|
|
9
|
+
|
|
10
|
+
CLONE="$HOME/000-projects/contributing-clanker/${GATE_REPO##*/}"
|
|
11
|
+
|
|
12
|
+
if [[ ! -d "$CLONE/.git" ]]; then
|
|
13
|
+
gate_skip "no local clone at $CLONE"
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
DEFAULT_BRANCH=$(fm_field "$GATE_DOSSIER_PATH" "default_branch")
|
|
17
|
+
[[ -z "$DEFAULT_BRANCH" ]] && DEFAULT_BRANCH="main"
|
|
18
|
+
|
|
19
|
+
CHANGED=$(/usr/bin/git -C "$CLONE" diff "$DEFAULT_BRANCH..HEAD" --name-only 2>/dev/null || /usr/bin/echo "")
|
|
20
|
+
|
|
21
|
+
if [[ -z "${CHANGED// /}" ]]; then
|
|
22
|
+
gate_pass "no changed files"
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
VENDOR_HITS=()
|
|
26
|
+
LOCKFILE_HITS=()
|
|
27
|
+
|
|
28
|
+
# Vendor-dir patterns (BLOCK)
|
|
29
|
+
VENDOR_RE='^(vendor/|node_modules/|\.next/|dist/|build/|generated/|_generated/)'
|
|
30
|
+
# Lockfile patterns (WARN)
|
|
31
|
+
LOCKFILE_RE='(^pnpm-lock\.yaml$|^package-lock\.json$|^Cargo\.lock$|^poetry\.lock$|^uv\.lock$|^yarn\.lock$|\.lock$)'
|
|
32
|
+
|
|
33
|
+
while IFS= read -r f; do
|
|
34
|
+
[[ -z "$f" ]] && continue
|
|
35
|
+
if /usr/bin/printf '%s' "$f" | /usr/bin/grep -qE "$VENDOR_RE"; then
|
|
36
|
+
VENDOR_HITS+=("$f")
|
|
37
|
+
elif /usr/bin/printf '%s' "$f" | /usr/bin/grep -qE "$LOCKFILE_RE"; then
|
|
38
|
+
LOCKFILE_HITS+=("$f")
|
|
39
|
+
fi
|
|
40
|
+
done <<< "$CHANGED"
|
|
41
|
+
|
|
42
|
+
if (( ${#VENDOR_HITS[@]} > 0 )); then
|
|
43
|
+
JOINED=$(/usr/bin/printf '%s, ' "${VENDOR_HITS[@]}" | /usr/bin/sed 's/, $//')
|
|
44
|
+
gate_block "vendored/generated paths edited: $JOINED" "regenerate from source instead of editing by hand; if this is intentional, override with --override-gate G1"
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
if (( ${#LOCKFILE_HITS[@]} > 0 )); then
|
|
48
|
+
JOINED=$(/usr/bin/printf '%s, ' "${LOCKFILE_HITS[@]}" | /usr/bin/sed 's/, $//')
|
|
49
|
+
gate_warn "lockfile changes detected: $JOINED" "verify the lockfile changes are intentional (dependency bumps) and not stale-checkout artifacts"
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
gate_pass "no vendored/generated path edits"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Catalog: G2 — diff touches infrastructure / CI paths that maintainers gate-keep
|
|
3
|
+
# Mitigates: drive-by edits to .github/workflows or terraform/ rarely land first try.
|
|
4
|
+
source "$(dirname "$0")/lib/preamble.sh"
|
|
5
|
+
|
|
6
|
+
gate_read_input
|
|
7
|
+
|
|
8
|
+
REPO_NAME="${GATE_REPO##*/}"
|
|
9
|
+
CLONE="$HOME/000-projects/contributing-clanker/$REPO_NAME"
|
|
10
|
+
|
|
11
|
+
if [[ ! -d "$CLONE/.git" ]]; then
|
|
12
|
+
gate_skip "no local clone at $CLONE"
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
DEFAULT_BRANCH=""
|
|
16
|
+
if [[ -n "$GATE_DOSSIER_PATH" && -f "$GATE_DOSSIER_PATH" ]]; then
|
|
17
|
+
DEFAULT_BRANCH=$(fm_field "$GATE_DOSSIER_PATH" "default_branch")
|
|
18
|
+
fi
|
|
19
|
+
[[ -z "$DEFAULT_BRANCH" ]] && DEFAULT_BRANCH="main"
|
|
20
|
+
|
|
21
|
+
CHANGED=$(/usr/bin/git -C "$CLONE" diff "$DEFAULT_BRANCH..HEAD" --name-only 2>/dev/null || /usr/bin/echo "")
|
|
22
|
+
|
|
23
|
+
PROTECTED=$(/usr/bin/printf '%s\n' "$CHANGED" | /usr/bin/grep -E '(\.github/workflows/|^infrastructure/|^terraform/|^helm/|^k8s/|^kubernetes/|^\.circleci/|^charts/)' || /usr/bin/echo "")
|
|
24
|
+
|
|
25
|
+
if [[ -z "$PROTECTED" ]]; then
|
|
26
|
+
gate_pass "diff does not touch protected infrastructure / CI paths"
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
LIST=$(/usr/bin/printf '%s' "$PROTECTED" | /usr/bin/tr '\n' ',' | /usr/bin/sed 's/,$//')
|
|
30
|
+
gate_warn "diff touches protected paths: $LIST" "infrastructure / CI changes deserve maintainer pre-approval; consider opening a Design Issue first to discuss the change"
|