@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,33 @@
1
+ #!/usr/bin/env bash
2
+ # Catalog: B3 — Local clone is stale vs origin/<default_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"
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
+ /usr/bin/timeout 30 /usr/bin/git fetch origin "$DEFAULT_BRANCH" --quiet 2>/dev/null || true
26
+
27
+ BEHIND=$(/usr/bin/git rev-list --count "HEAD..origin/$DEFAULT_BRANCH" 2>/dev/null || /usr/bin/echo "0")
28
+
29
+ if [[ "$BEHIND" -gt 100 ]]; then
30
+ gate_warn "local clone is $BEHIND commits behind origin/$DEFAULT_BRANCH" "rebase: git pull --rebase origin $DEFAULT_BRANCH"
31
+ fi
32
+
33
+ gate_pass "clone is current ($BEHIND commits behind origin/$DEFAULT_BRANCH)"
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env bash
2
+ # Catalog: B5 — Commits missing Signed-off-by when dossier requires DCO
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
+ DCO=$(fm_field "$GATE_DOSSIER_PATH" "dco_required")
12
+ if [[ "$DCO" != "true" ]]; then
13
+ gate_skip "dossier does not require DCO"
14
+ fi
15
+
16
+ DEFAULT_BRANCH=$(fm_field "$GATE_DOSSIER_PATH" "default_branch")
17
+ if [[ -z "$DEFAULT_BRANCH" ]]; then
18
+ gate_skip "no default_branch in dossier"
19
+ fi
20
+
21
+ REPO_NAME="${GATE_REPO##*/}"
22
+ CLONE_DIR="$HOME/000-projects/contributing-clanker/$REPO_NAME"
23
+
24
+ if [[ ! -d "$CLONE_DIR/.git" ]]; then
25
+ gate_skip "no local clone at $CLONE_DIR"
26
+ fi
27
+
28
+ cd "$CLONE_DIR" || gate_skip "cannot cd to clone dir"
29
+
30
+ # Iterate per-commit so we can flag exactly which sha is missing the trailer.
31
+ MISSING=""
32
+ COMMITS=$(/usr/bin/git rev-list "origin/$DEFAULT_BRANCH..HEAD" 2>/dev/null || /usr/bin/echo "")
33
+ if [[ -z "$COMMITS" ]]; then
34
+ gate_pass "no new commits to check"
35
+ fi
36
+
37
+ while read -r sha; do
38
+ [[ -z "$sha" ]] && continue
39
+ BODY=$(/usr/bin/git log -1 --format=%B "$sha" 2>/dev/null || /usr/bin/echo "")
40
+ if ! /usr/bin/printf '%s' "$BODY" | /usr/bin/grep -q "^Signed-off-by:"; then
41
+ MISSING="${MISSING}${sha:0:8} "
42
+ fi
43
+ done <<< "$COMMITS"
44
+
45
+ if [[ -n "$MISSING" ]]; then
46
+ COUNT=$(/usr/bin/printf '%s' "$COMMITS" | /usr/bin/grep -c .)
47
+ gate_block "commits missing Signed-off-by: $MISSING" "git commit --amend -s OR sign all commits: git rebase HEAD~$COUNT --signoff"
48
+ fi
49
+
50
+ gate_pass "all commits have Signed-off-by trailer"
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env bash
2
+ # Catalog: B6 — Commit subjects violate Conventional Commits when required
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
+ CC=$(fm_field "$GATE_DOSSIER_PATH" "conventional_commits")
12
+ if [[ "$CC" != "true" ]]; then
13
+ gate_skip "dossier does not require Conventional Commits"
14
+ fi
15
+
16
+ DEFAULT_BRANCH=$(fm_field "$GATE_DOSSIER_PATH" "default_branch")
17
+ if [[ -z "$DEFAULT_BRANCH" ]]; then
18
+ gate_skip "no default_branch in dossier"
19
+ fi
20
+
21
+ REPO_NAME="${GATE_REPO##*/}"
22
+ CLONE_DIR="$HOME/000-projects/contributing-clanker/$REPO_NAME"
23
+
24
+ if [[ ! -d "$CLONE_DIR/.git" ]]; then
25
+ gate_skip "no local clone at $CLONE_DIR"
26
+ fi
27
+
28
+ cd "$CLONE_DIR" || gate_skip "cannot cd to clone dir"
29
+
30
+ CC_REGEX='^[a-z]+(\([a-z0-9._/-]+\))?!?: .+'
31
+ FAILING=""
32
+
33
+ while IFS= read -r subj; do
34
+ [[ -z "$subj" ]] && continue
35
+ if ! /usr/bin/printf '%s' "$subj" | /usr/bin/grep -qE "$CC_REGEX"; then
36
+ FAILING="${FAILING} - ${subj}\n"
37
+ fi
38
+ done < <(/usr/bin/git log "origin/$DEFAULT_BRANCH..HEAD" --format=%s 2>/dev/null || /usr/bin/echo "")
39
+
40
+ if [[ -n "$FAILING" ]]; then
41
+ gate_block "commit subjects not Conventional Commits compliant" "fix subjects via git commit --amend or git rebase -i. Failing: $(/usr/bin/printf "$FAILING" | /usr/bin/tr '\n' '|')"
42
+ fi
43
+
44
+ gate_pass "all commit subjects match Conventional Commits"
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env bash
2
+ # Catalog: B7 — Local diff touches files outside candidate's claimed scope
3
+ source "$(dirname "$0")/lib/preamble.sh"
4
+
5
+ gate_read_input
6
+
7
+ if [[ -z "$GATE_CANDIDATE_PATH" || ! -f "$GATE_CANDIDATE_PATH" ]]; then
8
+ gate_skip "no candidate file"
9
+ fi
10
+
11
+ # Extract scope section from candidate body. Recognize "## Scope" or
12
+ # "## Files to touch" (case-insensitive on the heading word).
13
+ SCOPE_BLOCK=$(/usr/bin/awk '
14
+ BEGIN{flag=0}
15
+ /^## [Ss]cope/ {flag=1; next}
16
+ /^## [Ff]iles to touch/ {flag=1; next}
17
+ /^## / {flag=0}
18
+ flag {print}
19
+ ' "$GATE_CANDIDATE_PATH" 2>/dev/null || /usr/bin/echo "")
20
+
21
+ if [[ -z "$SCOPE_BLOCK" ]]; then
22
+ gate_skip "no '## Scope' or '## Files to touch' section in candidate"
23
+ fi
24
+
25
+ # Parse paths: strip blank lines, comments, list markers.
26
+ SCOPE_FILES=$(/usr/bin/printf '%s\n' "$SCOPE_BLOCK" \
27
+ | /usr/bin/sed -E 's/^[[:space:]]*[-*][[:space:]]+//; s/^[[:space:]]+//; s/[[:space:]]+$//' \
28
+ | /usr/bin/grep -vE '^(#|$)' \
29
+ | /usr/bin/sed -E 's/^`(.+)`$/\1/')
30
+
31
+ if [[ -z "$SCOPE_FILES" ]]; then
32
+ gate_skip "scope section present but empty"
33
+ fi
34
+
35
+ REPO_NAME="${GATE_REPO##*/}"
36
+ CLONE_DIR="$HOME/000-projects/contributing-clanker/$REPO_NAME"
37
+
38
+ if [[ ! -d "$CLONE_DIR/.git" ]]; then
39
+ gate_skip "no local clone at $CLONE_DIR"
40
+ fi
41
+
42
+ cd "$CLONE_DIR" || gate_skip "cannot cd to clone dir"
43
+
44
+ DEFAULT_BRANCH=""
45
+ if [[ -n "$GATE_DOSSIER_PATH" && -f "$GATE_DOSSIER_PATH" ]]; then
46
+ DEFAULT_BRANCH=$(fm_field "$GATE_DOSSIER_PATH" "default_branch")
47
+ fi
48
+ [[ -z "$DEFAULT_BRANCH" ]] && DEFAULT_BRANCH="main"
49
+
50
+ CHANGED=$(/usr/bin/git diff "origin/$DEFAULT_BRANCH..HEAD" --name-only 2>/dev/null || /usr/bin/echo "")
51
+
52
+ if [[ -z "$CHANGED" ]]; then
53
+ gate_pass "no changed files"
54
+ fi
55
+
56
+ DIVERGENT=""
57
+ while IFS= read -r f; do
58
+ [[ -z "$f" ]] && continue
59
+ if ! /usr/bin/printf '%s\n' "$SCOPE_FILES" | /usr/bin/grep -Fxq "$f"; then
60
+ DIVERGENT="${DIVERGENT}${f} "
61
+ fi
62
+ done <<< "$CHANGED"
63
+
64
+ if [[ -n "$DIVERGENT" ]]; then
65
+ gate_warn "diff touches files outside scope: $DIVERGENT" "either expand the candidate's scope section or revert the out-of-scope edits"
66
+ fi
67
+
68
+ gate_pass "all changed files are within declared scope"
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env bash
2
+ # Catalog: B12 — diff introduces new dependencies without prior issue conversation
3
+ # Mitigates: maintainers strongly prefer dep additions to be discussed first.
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
+ DIFF=$(/usr/bin/git -C "$CLONE" diff "$DEFAULT_BRANCH..HEAD" -- package.json Cargo.toml pyproject.toml requirements.txt go.mod 2>/dev/null || /usr/bin/echo "")
22
+
23
+ if [[ -z "$DIFF" ]]; then
24
+ gate_pass "no manifest files changed"
25
+ fi
26
+
27
+ # Heuristic: look for added dep lines across the supported manifest formats.
28
+ NEW_DEPS=$(/usr/bin/printf '%s\n' "$DIFF" \
29
+ | /usr/bin/grep -E '^\+([[:space:]]*"[A-Za-z0-9_@/.-]+"[[:space:]]*:[[:space:]]*"[^"]+",?$|[[:space:]]*[a-zA-Z][A-Za-z0-9_-]*[[:space:]]*=[[:space:]]*".*"$|[a-zA-Z][A-Za-z0-9_.-]*([<>=!~].*)?$)' \
30
+ | /usr/bin/grep -vE '^\+\+\+|^\+[[:space:]]*#|^\+[[:space:]]*"version"' \
31
+ | /usr/bin/sed -E 's/^\+[[:space:]]*//; s/[[:space:]]*=.*$//; s/^"//; s/".*$//; s/[<>=!~].*$//' \
32
+ | /usr/bin/grep -v '^[[:space:]]*$' \
33
+ || /usr/bin/echo "")
34
+
35
+ if [[ -z "$NEW_DEPS" ]]; then
36
+ gate_pass "no new dependency entries detected in manifest diffs"
37
+ fi
38
+
39
+ LIST=$(/usr/bin/printf '%s' "$NEW_DEPS" | /usr/bin/tr '\n' ',' | /usr/bin/sed 's/,$//')
40
+ gate_warn "diff appears to add new dependencies: $LIST" "new dependencies usually warrant an issue conversation first; check the issue thread for prior discussion"
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env bash
2
+ # Catalog: B14 — No recent green local check-run for current 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"
9
+ fi
10
+
11
+ CMD=$(fm_field "$GATE_DOSSIER_PATH" "local_check_command")
12
+ if [[ -z "$CMD" || "$CMD" == "(not detected)" ]]; then
13
+ gate_skip "no local_check_command in dossier"
14
+ fi
15
+
16
+ BRANCH=$(fm_field "$GATE_CANDIDATE_PATH" "branch")
17
+ if [[ -z "$BRANCH" ]]; then
18
+ gate_skip "no branch in candidate frontmatter"
19
+ fi
20
+
21
+ REPO_SLUG=$(/usr/bin/printf '%s' "$GATE_REPO" | /usr/bin/tr '/' '_' | /usr/bin/tr '/' '_')
22
+ # tr collapses single chars; doubled to satisfy the convention "owner/repo -> owner__repo"
23
+ REPO_SLUG=$(/usr/bin/printf '%s' "$GATE_REPO" | /usr/bin/sed 's|/|__|g')
24
+
25
+ EVIDENCE="$HOME/.contribute-system/check-runs/${REPO_SLUG}__${BRANCH}.json"
26
+
27
+ if [[ ! -f "$EVIDENCE" ]]; then
28
+ gate_block "no local check evidence for $BRANCH" "run the project's check command: $CMD"
29
+ fi
30
+
31
+ PASSED=$(jq -r '.passed // false' "$EVIDENCE" 2>/dev/null || /usr/bin/echo "false")
32
+ TS=$(jq -r '.ts // ""' "$EVIDENCE" 2>/dev/null || /usr/bin/echo "")
33
+
34
+ if [[ "$PASSED" != "true" ]]; then
35
+ gate_block "last local check run did not pass" "re-run after fixing: $CMD"
36
+ fi
37
+
38
+ if [[ -z "$TS" ]]; then
39
+ gate_warn "evidence file lacks timestamp" "re-run: $CMD"
40
+ fi
41
+
42
+ # Compare ts to now; warn if older than 1h.
43
+ NOW_EPOCH=$(/usr/bin/date -u +%s)
44
+ TS_EPOCH=$(/usr/bin/date -u -d "$TS" +%s 2>/dev/null || /usr/bin/echo "0")
45
+
46
+ if [[ "$TS_EPOCH" -eq 0 ]]; then
47
+ gate_warn "could not parse evidence ts=$TS" "re-run: $CMD"
48
+ fi
49
+
50
+ AGE=$(( NOW_EPOCH - TS_EPOCH ))
51
+ if [[ "$AGE" -gt 3600 ]]; then
52
+ gate_warn "last green check is $((AGE / 60))min old (>1h)" "re-run: $CMD"
53
+ fi
54
+
55
+ gate_pass "local checks passed at $TS"
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env bash
2
+ # Catalog: B16 — Dossier injection via local_check_command
3
+ # Mitigates: Security review GAP #1 — `local_check_command:` is free-text from
4
+ # upstream CONTRIBUTING.md. Malicious repo could inject `make test; rm -rf ~`.
5
+ # Briefing surfaces the recommendation, gate B14 may exec it. This gate refuses
6
+ # to proceed if the dossier's command doesn't match the safe-runner allowlist.
7
+ source "$(dirname "$0")/lib/preamble.sh"
8
+
9
+ gate_read_input
10
+
11
+ if [[ -z "$GATE_DOSSIER_PATH" || ! -f "$GATE_DOSSIER_PATH" ]]; then
12
+ gate_skip "no dossier — no command to validate"
13
+ fi
14
+
15
+ CMD=$(fm_field "$GATE_DOSSIER_PATH" "local_check_command")
16
+ if [[ -z "$CMD" || "$CMD" == "(not detected)" ]]; then
17
+ gate_pass "no local_check_command in dossier (nothing to validate)"
18
+ fi
19
+
20
+ # Allowlist: known-safe runners + their typical args. Composed commands (&&, ||,
21
+ # ;, |, $(), backticks) are denied unconditionally — they're the injection vector.
22
+ if /usr/bin/printf '%s' "$CMD" | /usr/bin/grep -qE '[;|`$(){}<>]|&&|\|\|'; then
23
+ gate_block "dossier local_check_command contains shell metacharacters: $CMD" "researcher-build.sh extracted this from upstream CONTRIBUTING.md. Treat as untrusted. Manually verify the command and edit the dossier; rerun this gate."
24
+ fi
25
+
26
+ # Allowlist regex: starts with a known-safe runner + simple alphanum args.
27
+ ALLOW_REGEX='^(make|pnpm|npm|yarn|cargo|pytest|python|python3|go|sbt|mix|dotnet|uv|hatch|tox|just|task|bundle|ruby) [a-zA-Z0-9 _:.,/\-]+$'
28
+ if ! /usr/bin/printf '%s' "$CMD" | /usr/bin/grep -qE "$ALLOW_REGEX" ; then
29
+ gate_block "dossier local_check_command not in allowlist: $CMD" "expected pattern: <make|pnpm|npm|yarn|cargo|pytest|...> followed by alphanumeric args. Edit the dossier to match the actual safe command, or override if you've manually verified."
30
+ fi
31
+
32
+ gate_pass "local_check_command matches safe-runner allowlist"
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env bash
2
+ # Catalog: C1 — Repo prefers draft PRs first; opening non-draft is anti-pattern
3
+ source "$(dirname "$0")/lib/preamble.sh"
4
+
5
+ gate_read_input
6
+
7
+ # Only relevant if dossier explicitly says draft_first: true
8
+ DRAFT_FIRST=$(fm_field "$GATE_DOSSIER_PATH" "draft_first")
9
+ if [[ "$DRAFT_FIRST" != "true" ]]; then
10
+ gate_skip "dossier does not require draft-first PRs"
11
+ fi
12
+
13
+ # Candidate's `draft:` field is set by the writer subagent (Slice 3)
14
+ DRAFT=$(fm_field "$GATE_CANDIDATE_PATH" "draft")
15
+ if [[ -z "$DRAFT" ]]; then
16
+ gate_skip "draft preference unknown — fill in candidate's \`draft:\` field before opening"
17
+ fi
18
+
19
+ if [[ "$DRAFT" == "false" ]]; then
20
+ gate_block "this repo prefers draft PRs first but candidate has draft: false" "this repo prefers draft PRs first; pass --draft to gh pr create OR set draft: true in the candidate"
21
+ fi
22
+
23
+ gate_pass "candidate set to open as draft (draft: true)"
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env bash
2
+ # Catalog: C2 — PR title violates repo-required regex
3
+ # Mitigates: maintainer immediately requests rename, signals "didn't read CONTRIBUTING".
4
+ source "$(dirname "$0")/lib/preamble.sh"
5
+
6
+ gate_read_input
7
+
8
+ if [[ -z "$GATE_DOSSIER_PATH" || ! -f "$GATE_DOSSIER_PATH" ]]; then
9
+ gate_skip "no dossier — cannot determine pr_title_regex"
10
+ fi
11
+
12
+ REGEX=$(fm_field "$GATE_DOSSIER_PATH" "pr_title_regex")
13
+ if [[ -z "$REGEX" ]]; then
14
+ gate_skip "dossier has no pr_title_regex"
15
+ fi
16
+
17
+ # Try frontmatter first
18
+ TITLE=$(fm_field "$GATE_CANDIDATE_PATH" "pr_title")
19
+
20
+ # Fall back to ## PR title section (single line content)
21
+ if [[ -z "$TITLE" ]]; then
22
+ TITLE=$(/usr/bin/awk '/^## PR title/{flag=1;next} /^## /{flag=0} flag' "$GATE_CANDIDATE_PATH" 2>/dev/null \
23
+ | /usr/bin/grep -m1 -v '^[[:space:]]*$' \
24
+ | /usr/bin/sed 's/^[[:space:]]*//;s/[[:space:]]*$//' \
25
+ || /usr/bin/echo "")
26
+ fi
27
+
28
+ if [[ -z "$TITLE" ]]; then
29
+ gate_skip "no PR title in candidate (frontmatter or ## PR title section)"
30
+ fi
31
+
32
+ if /usr/bin/printf '%s' "$TITLE" | /usr/bin/grep -qE "$REGEX"; then
33
+ gate_pass "PR title matches required regex"
34
+ fi
35
+
36
+ gate_block "PR title '$TITLE' does not match required regex /$REGEX/" "rename PR title to match /$REGEX/ per CONTRIBUTING.md"
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env bash
2
+ # Catalog: C3 — PR body draft missing sections required by the repo's PR template
3
+ source "$(dirname "$0")/lib/preamble.sh"
4
+
5
+ gate_read_input
6
+
7
+ # Pull the raw frontmatter line for pr_template_required_sections (a YAML array)
8
+ RAW=$(/usr/bin/awk '
9
+ /^---$/ { fm = !fm ? 1 : 2; next }
10
+ fm == 1 && /^pr_template_required_sections:/ {
11
+ sub(/^pr_template_required_sections:[[:space:]]*/, "")
12
+ print
13
+ exit
14
+ }
15
+ ' "$GATE_DOSSIER_PATH" 2>/dev/null || /usr/bin/echo "")
16
+
17
+ if [[ -z "$RAW" || "$RAW" == "[]" ]]; then
18
+ gate_skip "no pr_template_required_sections in dossier"
19
+ fi
20
+
21
+ # Strip surrounding [ ], split on commas, strip quotes/whitespace per item
22
+ INNER=$(/usr/bin/printf '%s' "$RAW" | /usr/bin/sed -E 's/^\[//; s/\]$//')
23
+ if [[ -z "$INNER" ]]; then
24
+ gate_skip "pr_template_required_sections is empty"
25
+ fi
26
+
27
+ # Build array of section names
28
+ declare -a SECTIONS=()
29
+ IFS=',' read -ra RAW_PARTS <<< "$INNER"
30
+ for part in "${RAW_PARTS[@]}"; do
31
+ clean=$(/usr/bin/printf '%s' "$part" | /usr/bin/sed -E 's/^[[:space:]]*"?//; s/"?[[:space:]]*$//')
32
+ [[ -n "$clean" ]] && SECTIONS+=("$clean")
33
+ done
34
+
35
+ if (( ${#SECTIONS[@]} == 0 )); then
36
+ gate_skip "no parseable section names in pr_template_required_sections"
37
+ fi
38
+
39
+ # Extract candidate's `## PR body` section
40
+ PR_BODY=$(/usr/bin/awk '/^## PR body/{flag=1;next} /^## /{flag=0} flag' "$GATE_CANDIDATE_PATH" 2>/dev/null || /usr/bin/echo "")
41
+ if [[ -z "$PR_BODY" ]]; then
42
+ gate_skip "no '## PR body' section in candidate yet"
43
+ fi
44
+
45
+ # Check each required section name appears as a markdown header (## or ###), case-insensitive
46
+ declare -a MISSING=()
47
+ for name in "${SECTIONS[@]}"; do
48
+ if ! /usr/bin/printf '%s' "$PR_BODY" | /usr/bin/grep -qiE "^#{2,3}[[:space:]]+${name}[[:space:]]*$"; then
49
+ MISSING+=("$name")
50
+ fi
51
+ done
52
+
53
+ if (( ${#MISSING[@]} > 0 )); then
54
+ joined=$(IFS=', '; /usr/bin/printf '%s' "${MISSING[*]}")
55
+ gate_block "PR body missing required sections: $joined" "add the section headers (## $joined) to your PR body draft"
56
+ fi
57
+
58
+ gate_pass "all required PR body sections present"
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env bash
2
+ # Catalog: C4 — PR touches UI files but PR body has no screenshot / recording
3
+ # Mitigates: maintainer asks "any screenshots?" → reads as not-quite-ready submission.
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
+ UI_FILES=$(/usr/bin/printf '%s\n' "$CHANGED" | /usr/bin/grep -E '\.(tsx|jsx|vue|svelte|html|css|scss)$' || /usr/bin/echo "")
23
+
24
+ if [[ -z "$UI_FILES" ]]; then
25
+ gate_pass "no UI files touched in diff"
26
+ fi
27
+
28
+ PR_BODY=$(/usr/bin/awk '/^## PR body/{flag=1;next} /^## /{flag=0} flag' "$GATE_CANDIDATE_PATH" 2>/dev/null || /usr/bin/echo "")
29
+
30
+ if [[ -z "$PR_BODY" ]]; then
31
+ gate_inform "UI files changed but no PR body drafted yet — remember to attach a screenshot or recording"
32
+ fi
33
+
34
+ if /usr/bin/printf '%s' "$PR_BODY" | /usr/bin/grep -qiE '!\[.*\]\(.*\)|screenshot|screen recording|\bgif\b|\.mp4|\.mov|\.webm|loom\.com|youtu\.be|youtube\.com'; then
35
+ gate_pass "PR body references screenshot or recording"
36
+ fi
37
+
38
+ gate_block "PR touches UI files but PR body has no screenshot or recording" "PRs touching UI need a screenshot or recording — paste an image (![alt](url)) or a Loom/GIF link in the PR body"
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env bash
2
+ # Catalog: C5 — PR body lacks fenced test-runner output (evidence of local verification)
3
+ source "$(dirname "$0")/lib/preamble.sh"
4
+
5
+ gate_read_input
6
+
7
+ # Extract candidate's `## PR body` section
8
+ PR_BODY=$(/usr/bin/awk '/^## PR body/{flag=1;next} /^## /{flag=0} flag' "$GATE_CANDIDATE_PATH" 2>/dev/null || /usr/bin/echo "")
9
+ if [[ -z "$PR_BODY" ]]; then
10
+ gate_skip "no '## PR body' section in candidate yet"
11
+ fi
12
+
13
+ # Walk fenced blocks (```...```) and check each for test-runner signals.
14
+ # Use awk to flip flag on ``` lines and collect block contents, separated by NUL-equivalent marker.
15
+ BLOCKS=$(/usr/bin/printf '%s' "$PR_BODY" | /usr/bin/awk '
16
+ /^```/ { in_block = !in_block; if (!in_block) print "<<<BLOCK_END>>>"; next }
17
+ in_block { print }
18
+ ')
19
+
20
+ if [[ -z "$BLOCKS" ]]; then
21
+ gate_block "PR body has no fenced code blocks" "paste your local test output as a fenced code block inside the PR body"
22
+ fi
23
+
24
+ # Test runner regexes — any one match in any block is sufficient
25
+ PATTERNS='pytest|cargo test|make test|pnpm test|npm test|yarn test|sbt test|go test|Test Suites:|[0-9]+ passed|[0-9]+ passing|[0-9]+ failed|^ok [0-9]+|PASS|FAIL'
26
+
27
+ if /usr/bin/printf '%s' "$BLOCKS" | /usr/bin/grep -qE "$PATTERNS"; then
28
+ gate_pass "PR body contains fenced block with test-runner output"
29
+ fi
30
+
31
+ gate_block "no fenced code block in PR body looks like test runner output" "paste your local test output as a fenced code block inside the PR body"
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env bash
2
+ # Catalog: C7 — `Co-Authored-By: Claude` trailer present in commits the repo has banned it
3
+ source "$(dirname "$0")/lib/preamble.sh"
4
+
5
+ gate_read_input
6
+
7
+ # Only run if dossier flags this repo as forbidding the Claude co-author trailer
8
+ FORBIDDEN=$(fm_field "$GATE_DOSSIER_PATH" "coauthor_claude_forbidden")
9
+ if [[ "$FORBIDDEN" != "true" ]]; then
10
+ gate_skip "dossier does not forbid Co-Authored-By: Claude"
11
+ fi
12
+
13
+ CLONE_DIR="$HOME/000-projects/contributing-clanker/${GATE_REPO##*/}"
14
+ if [[ ! -d "$CLONE_DIR/.git" ]]; then
15
+ gate_skip "no clone at $CLONE_DIR (not a git repo)"
16
+ fi
17
+
18
+ DEFAULT_BRANCH=$(fm_field "$GATE_DOSSIER_PATH" "default_branch")
19
+ if [[ -z "$DEFAULT_BRANCH" ]]; then
20
+ gate_skip "no default_branch in dossier"
21
+ fi
22
+
23
+ # Inspect commit messages between default branch and HEAD on the working clone
24
+ LOG=$(/usr/bin/git -C "$CLONE_DIR" log "${DEFAULT_BRANCH}..HEAD" --format=%B 2>/dev/null || /usr/bin/echo "")
25
+
26
+ if /usr/bin/printf '%s' "$LOG" | /usr/bin/grep -qiE "Co-Authored-By:[[:space:]]+Claude"; then
27
+ gate_block "commits contain 'Co-Authored-By: Claude' trailer" "remove via git rebase -i and dropping the trailer; this repo's AI policy forbids it"
28
+ fi
29
+
30
+ gate_pass "no Co-Authored-By: Claude trailer in commits"
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env bash
2
+ # Catalog: C9 — PR body lacks `Closes #N` / `Fixes #N` referencing the candidate
3
+ # Mitigates: PR doesn't auto-close the issue on merge → maintainer has to do it
4
+ # manually → reads as low-effort.
5
+ source "$(dirname "$0")/lib/preamble.sh"
6
+
7
+ gate_read_input
8
+
9
+ # This gate runs at open-pr / flip-to-ready transitions and needs the PR body.
10
+ # At this stage the PR body lives in the candidate's `pr_body_draft:` field
11
+ # (written by writer subagent in Slice 3). For now, look in the candidate's
12
+ # `## PR body` section if present.
13
+ ISSUE_NUM=$(fm_field "$GATE_CANDIDATE_PATH" "issue_number")
14
+ if [[ -z "$ISSUE_NUM" ]]; then
15
+ gate_skip "no issue_number in candidate (cannot verify link)"
16
+ fi
17
+
18
+ # Extract PR body draft from candidate's body sections
19
+ PR_BODY=$(/usr/bin/awk '/^## PR body/{flag=1;next} /^## /{flag=0} flag' "$GATE_CANDIDATE_PATH" 2>/dev/null || /usr/bin/echo "")
20
+
21
+ if [[ -z "$PR_BODY" ]]; then
22
+ # No PR body drafted yet — gate is informational at pre-draft stages
23
+ gate_inform "no PR body drafted yet (candidate has no '## PR body' section)"
24
+ fi
25
+
26
+ # Look for any of: Closes #N, Fixes #N, Resolves #N (case-insensitive)
27
+ if /usr/bin/printf '%s' "$PR_BODY" | /usr/bin/grep -qiE "(closes|fixes|resolves)[[:space:]]+#?$ISSUE_NUM\b"; then
28
+ gate_pass "PR body links issue #$ISSUE_NUM via auto-close keyword"
29
+ fi
30
+
31
+ gate_block "PR body does not reference issue #$ISSUE_NUM with an auto-close keyword" "add 'Closes #$ISSUE_NUM' (or Fixes/Resolves) to the PR body so the issue auto-closes on merge."
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env bash
2
+ # Catalog: C11 — branch divergence implies force-push will be required
3
+ # Mitigates: collaborators with local refs to this branch get rebased out from under them.
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
+ BRANCH=$(fm_field "$GATE_CANDIDATE_PATH" "branch")
16
+ if [[ -z "$BRANCH" ]]; then
17
+ gate_skip "no branch in candidate frontmatter"
18
+ fi
19
+
20
+ # Confirm the upstream tracking ref exists
21
+ if ! /usr/bin/git -C "$CLONE" rev-parse --verify "origin/$BRANCH" >/dev/null 2>&1; then
22
+ gate_skip "no origin/$BRANCH tracking ref — first push, force not implied"
23
+ fi
24
+
25
+ AHEAD=$(/usr/bin/git -C "$CLONE" log "origin/$BRANCH..HEAD" --oneline 2>/dev/null | /usr/bin/grep -c . || /usr/bin/echo 0)
26
+ BEHIND=$(/usr/bin/git -C "$CLONE" log "HEAD..origin/$BRANCH" --oneline 2>/dev/null | /usr/bin/grep -c . || /usr/bin/echo 0)
27
+
28
+ if [[ "$AHEAD" -gt 0 && "$BEHIND" -gt 0 ]]; then
29
+ gate_warn "branch diverges from origin/$BRANCH — push will require force; ensure no other contributors have local refs to this branch" "push directly only if you're the sole owner of this branch"
30
+ fi
31
+
32
+ gate_pass "branch is fast-forward to origin/$BRANCH (no force-push needed)"
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env bash
2
+ # Catalog: C12 — CI not green (any failed/pending checks block flip-to-ready)
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
+ CHECKS_JSON=$(gh_safe pr checks "$PR_NUMBER" --repo "$GATE_REPO" --json bucket || /usr/bin/echo "[]")
13
+ if [[ -z "$CHECKS_JSON" ]]; then
14
+ CHECKS_JSON="[]"
15
+ fi
16
+
17
+ FAIL_COUNT=$(/usr/bin/printf '%s' "$CHECKS_JSON" | jq '[.[] | select(.bucket == "fail")] | length' 2>/dev/null || /usr/bin/echo "0")
18
+ PENDING_COUNT=$(/usr/bin/printf '%s' "$CHECKS_JSON" | jq '[.[] | select(.bucket == "pending")] | length' 2>/dev/null || /usr/bin/echo "0")
19
+
20
+ if [[ "$FAIL_COUNT" != "0" ]]; then
21
+ FAILED=$(/usr/bin/printf '%s' "$CHECKS_JSON" | jq -r '[.[] | select(.bucket == "fail") | .name] | join(", ")' 2>/dev/null || /usr/bin/echo "")
22
+ gate_block "CI has $FAIL_COUNT failing check(s): $FAILED" "fix the failing checks before flipping to ready-for-review"
23
+ fi
24
+
25
+ if [[ "$PENDING_COUNT" != "0" ]]; then
26
+ gate_warn "$PENDING_COUNT CI check(s) still pending" "wait for CI to finish before flipping to ready-for-review"
27
+ fi
28
+
29
+ gate_pass "all CI checks green"