@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,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 () 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"
|