@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.
Files changed (69) hide show
  1. package/README.md +173 -0
  2. package/hooks/.gitkeep +0 -0
  3. package/hooks/install.sh +115 -0
  4. package/hooks/uninstall.sh +70 -0
  5. package/package.json +42 -0
  6. package/skills/contribute/SKILL.md +457 -0
  7. package/skills/contribute/agents/draft-writer.md +110 -0
  8. package/skills/contribute/agents/repo-analyzer.md +68 -0
  9. package/skills/contribute/agents/researcher.md +246 -0
  10. package/skills/contribute/agents/scout.md +182 -0
  11. package/skills/contribute/agents/test-runner.md +70 -0
  12. package/skills/contribute/assets/claim-template.md +22 -0
  13. package/skills/contribute/assets/evidence-template.md +32 -0
  14. package/skills/contribute/assets/pr-template.md +49 -0
  15. package/skills/contribute/references/candidate-file-format.md +259 -0
  16. package/skills/contribute/references/workflow-guide.md +153 -0
  17. package/skills/contribute/scripts/audit-overrides.sh +180 -0
  18. package/skills/contribute/scripts/catalog-coverage.sh +99 -0
  19. package/skills/contribute/scripts/gate-runner.sh +191 -0
  20. package/skills/contribute/scripts/gates/a01-already-assigned.sh +24 -0
  21. package/skills/contribute/scripts/gates/a02-already-shipped.sh +31 -0
  22. package/skills/contribute/scripts/gates/a03-duplicate-flagged.sh +22 -0
  23. package/skills/contribute/scripts/gates/a04-issue-age.sh +80 -0
  24. package/skills/contribute/scripts/gates/a05-issue-still-open.sh +33 -0
  25. package/skills/contribute/scripts/gates/a06-claim-etiquette-required.sh +36 -0
  26. package/skills/contribute/scripts/gates/a09-mention-routing.sh +63 -0
  27. package/skills/contribute/scripts/gates/b01-base-branch.sh +29 -0
  28. package/skills/contribute/scripts/gates/b02-branch-naming.sh +34 -0
  29. package/skills/contribute/scripts/gates/b03-clone-fresh.sh +33 -0
  30. package/skills/contribute/scripts/gates/b05-dco-signoff.sh +50 -0
  31. package/skills/contribute/scripts/gates/b06-commit-format.sh +44 -0
  32. package/skills/contribute/scripts/gates/b07-scope-files.sh +68 -0
  33. package/skills/contribute/scripts/gates/b12-new-deps.sh +40 -0
  34. package/skills/contribute/scripts/gates/b14-local-checks.sh +55 -0
  35. package/skills/contribute/scripts/gates/b16-local-check-allowlist.sh +32 -0
  36. package/skills/contribute/scripts/gates/c01-draft-first.sh +23 -0
  37. package/skills/contribute/scripts/gates/c02-pr-title-format.sh +36 -0
  38. package/skills/contribute/scripts/gates/c03-pr-body-sections.sh +58 -0
  39. package/skills/contribute/scripts/gates/c04-ui-screenshots.sh +38 -0
  40. package/skills/contribute/scripts/gates/c05-test-evidence.sh +31 -0
  41. package/skills/contribute/scripts/gates/c07-coauthor-banned.sh +30 -0
  42. package/skills/contribute/scripts/gates/c09-issue-link.sh +31 -0
  43. package/skills/contribute/scripts/gates/c11-no-force-push.sh +32 -0
  44. package/skills/contribute/scripts/gates/c12-ci-green.sh +29 -0
  45. package/skills/contribute/scripts/gates/c13-bots-passed.sh +62 -0
  46. package/skills/contribute/scripts/gates/c16-no-self-merge.sh +24 -0
  47. package/skills/contribute/scripts/gates/c19-body-claim-vs-diff.sh +64 -0
  48. package/skills/contribute/scripts/gates/d02-no-ai-bug-reports.sh +48 -0
  49. package/skills/contribute/scripts/gates/d03-no-ai-pr-reviews.sh +42 -0
  50. package/skills/contribute/scripts/gates/d05-no-reopen.sh +25 -0
  51. package/skills/contribute/scripts/gates/e02-ai-strike-track.sh +57 -0
  52. package/skills/contribute/scripts/gates/e04-fork-target.sh +39 -0
  53. package/skills/contribute/scripts/gates/f01-license-compat.sh +92 -0
  54. package/skills/contribute/scripts/gates/f03-fixtures-clean.sh +30 -0
  55. package/skills/contribute/scripts/gates/f04-override-disclosure.sh +49 -0
  56. package/skills/contribute/scripts/gates/g01-no-vendored-edits.sh +52 -0
  57. package/skills/contribute/scripts/gates/g02-protected-paths.sh +30 -0
  58. package/skills/contribute/scripts/gates/g03-no-changelog-edits.sh +37 -0
  59. package/skills/contribute/scripts/gates/g04-no-version-bump.sh +36 -0
  60. package/skills/contribute/scripts/gates/g06-override-rate-limit.sh +31 -0
  61. package/skills/contribute/scripts/gates/lib/preamble.sh +105 -0
  62. package/skills/contribute/scripts/lint-candidate.sh +149 -0
  63. package/skills/contribute/scripts/researcher-build.sh +456 -0
  64. package/skills/contribute/scripts/test-known-traps.sh +142 -0
  65. package/skills/contribute/scripts/test-override-audit.sh +102 -0
  66. package/skills/contribute/scripts/test-plug-in.sh +113 -0
  67. package/skills/contribute/scripts/test-scout-refresh.sh +157 -0
  68. package/skills/contribute/scripts/test-stale-dossier-refresh.sh +96 -0
  69. 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"