Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
3af327b
feat: complexity-gated AI review triggers after CR rounds (#362)
cursoragent May 1, 2026
2991d70
fix: address CR/BugBot review — env precedence, dedupe state, SC2034
cursoragent May 1, 2026
7edcadd
fix: add top-level heading and ini language tag to pm-config.md (MD04…
auerbachb May 2, 2026
17cf83d
fix: rule-lint trim, pm-config constraints, post_one state-before-post
auerbachb May 4, 2026
bb1751e
fix: exit-code propagation, post-then-persist ordering, invalid FILE_…
auerbachb May 4, 2026
abd13f7
fix: fail-closed overrides for THRESHOLD/FIRST_CR_ROUND/CADENCE, earl…
auerbachb May 4, 2026
c3ca07f
fix: reject leading-zero config values and pass PR# to /pr-review-help
auerbachb May 4, 2026
dac0377
fix: document THRESHOLD_SCORE type constraint and guard helper execut…
auerbachb May 4, 2026
4d891e1
fix: revert /pr-review-help to no-arg form per SKILL.md spec
auerbachb May 4, 2026
518194a
fix: restore PR number in /pr-review-help trigger; correct SKILL.md doc
auerbachb May 4, 2026
17e19c8
fix: use per-key --set in post_one to prevent stale-snapshot overwrites
auerbachb May 4, 2026
6d34106
fix(cycle-count): preserve full review stream in --cr-only boundary calc
auerbachb May 5, 2026
0473032
fix: correct doc for ENABLE_PR_REVIEW_HELP and standardize issue ref …
auerbachb May 5, 2026
8a95393
fix: clarify #362 trigger guard, STATE_FILE validation, and reviewer …
auerbachb May 5, 2026
9ca97b2
chore(rules): trim 29 words to satisfy hard cap (#362)
auerbachb May 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .claude/pm-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# PM Config

## Role

<!-- Optional: who uses this config (human-editable). -->

## OKRs

No OKRs set — add objectives under this header when ready.

## Complexity triggers

<!-- Issue #362 — tune per repo. Defaults match claude-code-config calibration (25 merged PRs, threshold 100 → 72% would exceed). -->

```ini
THRESHOLD_SCORE=100
FIRST_CR_ROUND=3
CADENCE_ROUNDS=2
FILE_WEIGHT=5
ENABLE_PR_REVIEW_HELP=0
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- **THRESHOLD_SCORE** — minimum `complexity-score.sh` value before auto-trigger; must be a **non-negative integer**. Repo file sets the default; **`COMPLEXITY_THRESHOLD_SCORE` env overrides** when set.
- **FIRST_CR_ROUND** — first fire at this CodeRabbit round count (must be **≥ 3**; scripts error out otherwise — needs ≥ 2 completed CR rounds before first fire). Uses `cycle-count.sh <PR> --cr-only`. **`COMPLEXITY_FIRST_CR_ROUND` env overrides** when set.
- **CADENCE_ROUNDS** — after the first fire, fire again every N additional CR rounds (e.g. 2 → rounds 3, 5, 7…); must be **≥ 1**. **`COMPLEXITY_CADENCE_ROUNDS` env overrides** when set.
- **FILE_WEIGHT** — multiplier on `changedFiles` inside the score; must be a **positive integer** (0 and non-positive values are rejected). **`COMPLEXITY_FILE_WEIGHT` env overrides** when set.
- **ENABLE_PR_REVIEW_HELP** — `1` / `true` / `yes` / `on` posts a fourth comment `/pr-review-help #<PR_NUMBER>` after the three single-mention triggers.

## Team

<!-- Optional: contributor display names. -->

## Notes

<!-- Free-form. -->
12 changes: 7 additions & 5 deletions .claude/rules/cr-github-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
>
> **Always:** Poll all 3 endpoints + check-runs every cycle. Use `per_page=100`. Filter by `coderabbitai[bot]`. Batch fixes into one commit. Reply to every thread. Resolve threads via GraphQL. **Enter the polling loop immediately after push — do NOT ask.** Invoke `/fixpr` when any trigger condition fires (see "Per-cycle check" below).
> **Ask first:** Merging — always ask the user. **Nothing else in this workflow requires permission.**
> **Never:** Poll only 1-2 endpoints. Use bare `coderabbitai` without `[bot]`. Push per-finding. Trigger `@coderabbitai full review` more than twice per PR per hour. Trigger Greptile proactively (only on CR failure). Merge without meeting the merge gate (see `cr-merge-gate.md` for the authoritative definition). **Exit polling on "nothing unresolved right now" — the only valid exit is the merge gate.** **Ask "want me to poll?" or "should I process this feedback?" — just do it.**
> **Never:** Poll only 1-2 endpoints. Use bare `coderabbitai` without `[bot]`. Push per-finding. Trigger `@coderabbitai full review` more than twice per PR per hour. Trigger Greptile proactively (only on CR failure). Merge without meeting the merge gate (see `cr-merge-gate.md` for the authoritative definition). **Exit polling on "nothing unresolved right now" — the only valid exit is the merge gate.**
>
> **This fallback workflow runs after every push/PR.** Local review reduces findings; it does not replace GitHub review.

Expand Down Expand Up @@ -35,17 +35,19 @@ Run this before the first poll tick and before any new review trigger (`@coderab

### Per-cycle check (every 60 seconds)

Each cycle, query everything in "Polling" for every open PR owned by this session. **Re-read current HEAD SHA every cycle** so stale approvals never exit polling.
Each cycle, query everything in Polling for every open PR owned by this session. **Re-read current HEAD SHA every cycle** so stale approvals never exit polling.

If **ANY** of the conditions below hold, invoke `/fixpr` and do NOT request a new review until `/fixpr` completes:

1. New bot findings since the last poll watermark (not old unresolved threads awaiting reviewer ack)
2. Any check-run with a blocking conclusion (`failure`, `timed_out`, `action_required`, `startup_failure`, `stale`)
3. **`mergeStateStatus == "BEHIND"`** — each cycle, read this field explicitly (e.g. `gh pr view <N> --json mergeStateStatus,mergeable` or the PR snapshot used for polling). **Do not treat `mergeStateStatus: "BLOCKED"` as “behind base”**; BLOCKED covers missing checks/reviews as well. Only the literal value `BEHIND` triggers rebase + force-push via `/fixpr` (same merge-state handling as `/fixpr` Step 6 / `.merge_state` from `pr-state.sh` — see `fixpr/SKILL.md`).
4. `mergeable == "CONFLICTING"` (merge conflicts; `/fixpr` handles rebase + surfaces blockers)
3. **`mergeStateStatus == BEHIND`** — each cycle, read this field explicitly (e.g. `gh pr view <N> --json mergeStateStatus,mergeable` or the PR snapshot used for polling). **Do not treat `mergeStateStatus: BLOCKED` as “behind base”**; BLOCKED covers missing checks/reviews as well. Only the literal value `BEHIND` triggers rebase + force-push via `/fixpr` (same merge-state handling as `/fixpr` Step 6 / `.merge_state` from `pr-state.sh` — see `fixpr/SKILL.md`).
4. `mergeable == CONFLICTING` (merge conflicts; `/fixpr` handles rebase + surfaces blockers)

> **Unresolved threads are NOT a trigger.** After a fix push, keep polling for reviewer catch-up unless conditions 1-4 occur.

**#362:** If this cycle does not require `/fixpr` **and** the session-start / pre-review audit has confirmed there are no unresolved bot findings for the current SHA, run `maybe-trigger-ai-review.sh <PR>` (`pm-config.md` **Complexity triggers**; `complexity-score.sh`; `cycle-count.sh --cr-only`; three separate `@` comments: `@codeant-ai review`, `@cursor review`, `@graphite-app re-review`). Dedupe `session-state.json` `.prs[N].ai_review_trigger_*`.

**SHA freshness (every cycle).** A CR approval must have `.commit_id == current HEAD SHA`; otherwise it is stale. Re-trigger (respecting the 2/hour cap) and keep polling. See `cr-merge-gate.md` for retraction rules.

**Exit polling ONLY when the merge gate (`cr-merge-gate.md`) is met.** "0 unresolved threads right now" is NOT an exit condition — see the trap note at the top of this file. After any `/fixpr` push, reset the watermark and keep polling for the reviewer's response to the new SHA.
Expand Down Expand Up @@ -81,7 +83,7 @@ Verdicts: `polling_cr`, `switch_bugbot`, `trigger_greptile`, `budget_exhausted`,
- **Fast-path rate limit:** "rate limit" in failed CodeRabbit check/status output goes through the escalation gate above. Sticky assignment applies.
- **Ack ≠ completion.** "Actions performed — Full review triggered" = CR started. "CodeRabbit — Review completed" CI check = CR finished.
- **CR username:** `coderabbitai[bot]` (with `[bot]` suffix). Filter by `.user.login == "coderabbitai[bot]"` — NOT bare `coderabbitai`.
- **Watermark:** Track highest review ID from `pulls/{N}/reviews`. New reviews can have inline comment IDs lower than previous reviews (different ID sequences). For `issues/{N}/comments`, track by comment ID.
- **Watermark:** Track highest review ID from `pulls/{N}/reviews`. New reviews can have inline comment IDs lower than previous reviews. For `issues/{N}/comments`, track by comment ID.
- **CR silence threshold:** Cadence 60 s; a CR `status: "completed"` exits polling immediately. Otherwise, the escalation gate owns silence, BugBot grace, and Greptile fallback. Sticky assignment applies.

### CI Health Check (MANDATORY — every poll cycle)
Expand Down
4 changes: 2 additions & 2 deletions .claude/rules/cr-merge-gate.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ The merge gate depends on which reviewer owns the PR:

### Code-owner bots

Some repos list CR (`@coderabbitai`) or Greptile (`@greptile-apps`) in `CODEOWNERS`. When branch protection has `require_code_owner_reviews`, that bot's `APPROVED` review on the current HEAD SHA satisfies the code-owner approval requirement. Do not ask the PR author or repo owner to self-approve — GitHub does not allow author self-approval and the bot approval is the terminal unblock when fresh.
Some repos list CR (`@coderabbitai`) or Greptile (`@greptile-apps`) in `CODEOWNERS`. When branch protection has `require_code_owner_reviews`, that bot's `APPROVED` review on the current HEAD SHA satisfies the code-owner approval requirement. Do not ask the PR author to self-approve; the bot approval is the terminal unblock when fresh.

Because `CODEOWNERS` varies by repo, this is a runtime check. `.claude/scripts/merge-gate.sh` reads `CODEOWNERS`, `.github/CODEOWNERS`, or `docs/CODEOWNERS`; when CR, Greptile, or **CodeAnt** (`@codeant-ai`) is a code owner it also requires GitHub `reviewDecision == "APPROVED"` on the current PR head. If branch protection is `BLOCKED` and a prior bot approval is stale/dismissed after a push, trigger that bot again (`@coderabbitai full review` for CR, `@greptileai` for Greptile, `@codeant-ai review` for CodeAnt) and keep polling. Human escalation is only for an actual human-authored `CHANGES_REQUESTED`, not stale bot approval.

Expand Down Expand Up @@ -103,7 +103,7 @@ Every thread must be `isResolved: true` via GraphQL `reviewThreads` (REST misses
> 5. If any item fails, fix the code first — do NOT offer to merge with unchecked boxes
> 6. Only after **ALL** boxes are checked, proceed to Step 3
>
> Re-run after every CR round. If additional code changes were made during the CR loop (e.g. fixes from CR rounds after the initial AC pass), you must re-verify ALL AC items against the final code. AC verification reflects the code **at merge time**, not the code at some earlier checkpoint.
> Re-run after every review round. Re-verify all AC items if code changed during review; verification reflects the code **at merge time**, not an earlier checkpoint.
>
> Skipping this step is a **blocking failure** — the user should never see unchecked AC boxes when asked about merge.

Expand Down
4 changes: 3 additions & 1 deletion .claude/scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ Manually-invoked utility scripts. Run these from the command line when needed.
| `repair-trust-single.sh <absolute-project-path>` | Fix trust flags for one project in `~/.claude.json` |
| `repair-trust-all.sh` | Fix trust flags for all projects in `~/.claude.json` |
| `repair-worktrees.sh [--apply]` | Detect stale git worktrees (branch merged to main or deleted on origin) and optionally remove them. Dry-run by default; skips worktrees with uncommitted changes and never touches the main worktree. |
| `cycle-count.sh <pr_number> [--exclude-bots]` | Reconstruct per-PR review-then-fix cycle count. Prints an integer on stdout. Used by `/merge`, `/wrap`, `/pm-rate-team`, `/pm-sprint-review`. See `--help` and `.claude/reference/pm-data-patterns.md` "Review cycles per PR". |
| `cycle-count.sh <pr_number> [--exclude-bots] [--cr-only]` | Reconstruct per-PR review-then-fix cycle count. Prints an integer on stdout. **`--cr-only`** restricts to CodeRabbit (`coderabbitai[bot]`) for issue #362 round gating. Used by `/merge`, `/wrap`, `/pm-rate-team`, `/pm-sprint-review`. See `--help` and `.claude/reference/pm-data-patterns.md` "Review cycles per PR". |
| `complexity-score.sh <pr_number> [--json]` | PR complexity score: `additions + deletions + file_weight × changedFiles` (default `file_weight=5`, tunable via `.claude/pm-config.md` **Complexity triggers**). Used by `maybe-trigger-ai-review.sh`. |
| `maybe-trigger-ai-review.sh <pr_number> [--dry-run] [--json]` | Issue #362: when complexity + CR-round gates pass, posts three **separate** PR comments (`@codeant-ai review`, `@cursor review`, `@graphite-app re-review`); optional `/pr-review-help`. Reads thresholds from `.claude/pm-config.md`. |
| `audit-skill-usage.sh` | Legacy monthly skill-usage audit against `.claude/data/skill-usage.json` (not fed by the PostToolUse hook). |
| `skill-usage-report.sh [--days N]` | Reads `~/.claude/skill-usage.log` (tab: UTC time, skill, session_id), prints markdown tables and dead-skill candidates (90d stale or never invoked after 30d of telemetry). |
| `resolve-review-threads.sh <pr_number> [--authors a,b,c] [--thread-ids ids\|--thread-ids-file path] [--max-attempts N] [--dry-run] [--verify-only]` | Fetch PR review threads via GraphQL, filter to bot authors or an explicit addressed-thread set, resolve via `resolveReviewThread` (fallback: `minimizeComment`), then re-query and verify `isResolved=true`. `--verify-only` with explicit thread ids re-fetches and exits non-zero if any id is missing or still unresolved (no mutations); an empty explicit id list is a no-op success. Used by `/fixpr`, `/go-on`, and `phase-a-fixer`. Exit codes: 0 all addressed/matching threads verified resolved, 1 ≥1 dangling thread remains after retries/fallback (or verify-only failed), 2 usage, 3 PR not found, 4 gh error. |
Expand Down
125 changes: 125 additions & 0 deletions .claude/scripts/complexity-score.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
#!/usr/bin/env bash
# complexity-score.sh — PR complexity score for issue #362 auto-triggers.
#
# Formula (documented in `.claude/rules/cr-github-review.md`):
# score = additions + deletions + (file_weight × changedFiles)
#
# Default file_weight=5 matches calibration on this repo (merged PRs sample);
# override via `.claude/pm-config.md` section **Complexity triggers** (`FILE_WEIGHT=N`)
# or env `COMPLEXITY_FILE_WEIGHT`.
#
# Usage:
# complexity-score.sh <pr_number> [--json]
# complexity-score.sh --help | -h
#
# Exit: 0 OK, 2 usage, 3 PR not found, 4 gh/jq error

set -euo pipefail
printf '%s\t%s\t%s\n' "$(date -u +%FT%TZ)" "$(basename "$0")" "${*//$'\n'/ }" >> "$HOME/.claude/script-usage.log" 2>/dev/null || true

help() {
sed -n '2,15p' "$0" | sed 's/^# \{0,1\}//'
}

PR_NUM=""
JSON_OUT=0
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) help; exit 0 ;;
--json) JSON_OUT=1; shift ;;
-*)
echo "complexity-score.sh: unknown flag: $1" >&2
exit 2
;;
*)
if [[ -n "$PR_NUM" ]]; then
echo "complexity-score.sh: unexpected argument: $1" >&2
exit 2
fi
PR_NUM="$1"
shift
;;
esac
done

if [[ -z "$PR_NUM" ]] || ! [[ "$PR_NUM" =~ ^[1-9][0-9]*$ ]]; then
echo "usage: complexity-score.sh <pr_number> [--json]" >&2
exit 2
fi

if ! command -v gh >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then
echo "complexity-score.sh: requires gh and jq" >&2
exit 4
fi

FILE_WEIGHT=5

# Optional: read FILE_WEIGHT from repo .claude/pm-config.md (Complexity triggers section)
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
PM_CFG=""
[[ -n "$REPO_ROOT" && -f "$REPO_ROOT/.claude/pm-config.md" ]] && PM_CFG="$REPO_ROOT/.claude/pm-config.md"
if [[ -n "$PM_CFG" ]]; then
section=$(awk '/^## Complexity triggers/,/^## / { if (/^## Complexity triggers/) next; if (/^## /) exit; print }' "$PM_CFG" 2>/dev/null || true)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The section extractor uses an awk range that starts at ## Complexity triggers and also ends on any ## ... header, so it terminates on the same header line and returns an empty section. That makes FILE_WEIGHT from pm-config.md effectively unreadable, so repo config is silently ignored unless env override is set. Change the section parsing to skip the first matching header and stop only on the next header. [incorrect condition logic]

Severity Level: Major ⚠️
- ⚠️ PR complexity score ignores FILE_WEIGHT in pm-config.
- ⚠️ maybe-trigger thresholds rely on default file weight only.
Steps of Reproduction ✅
1. Open `.claude/pm-config.md` and observe the `## Complexity triggers` section with an
`ini` block defining `FILE_WEIGHT`, e.g. at lines 11–21 (`FILE_WEIGHT=5`) in
`/workspace/claude-code-config/.claude/pm-config.md`.

2. Note that `.claude/scripts/complexity-score.sh` attempts to read that section at line
62 via `section=$(awk '/^## Complexity triggers/,/^## / { if (/^## Complexity triggers/)
next; if (/^## /) exit; print }' "$PM_CFG" ...)`.

3. Consider how awk range patterns work: the range `/^## Complexity triggers/,/^## /`
starts on the `## Complexity triggers` header line and ends on the first line matching
`^## `; for this file the *same* header line matches both patterns, so the range is active
only for that single record, and awk's range logic turns the range off immediately after
that record.

4. On the header line, the inner awk block executes `if (/^## Complexity triggers/)
next;`, which skips printing the header, and because the range ends on that same line, no
lines from the fenced `ini` block (including `FILE_WEIGHT=...`) are ever in-range;
`section` is therefore always empty, so `FILE_WEIGHT` from `pm-config.md` is never applied
and the script always uses the default `FILE_WEIGHT=5` unless `COMPLEXITY_FILE_WEIGHT` is
set.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** .claude/scripts/complexity-score.sh
**Line:** 62:62
**Comment:**
	*Incorrect Condition Logic: The section extractor uses an awk range that starts at `## Complexity triggers` and also ends on any `## ...` header, so it terminates on the same header line and returns an empty section. That makes `FILE_WEIGHT` from `pm-config.md` effectively unreadable, so repo config is silently ignored unless env override is set. Change the section parsing to skip the first matching header and stop only on the *next* header.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

if [[ -n "$section" ]]; then
line=$(echo "$section" | grep -E '^[[:space:]]*FILE_WEIGHT[[:space:]]*=' | head -1 || true)
if [[ -n "$line" ]]; then
val="${line#*=}"
val="${val// /}"
val="${val//$'\t'/}"
if [[ "$val" =~ ^[1-9][0-9]*$ ]]; then
FILE_WEIGHT="$val"
else
echo "complexity-score.sh: pm-config FILE_WEIGHT='$val' is not a positive integer" >&2
exit 4
fi
fi
fi
fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
auerbachb marked this conversation as resolved.

# Env wins when set (explicit override for CI / one-off tuning).
if [[ "${COMPLEXITY_FILE_WEIGHT+set}" == "set" ]]; then
if [[ "${COMPLEXITY_FILE_WEIGHT}" =~ ^[1-9][0-9]*$ ]]; then
FILE_WEIGHT="$COMPLEXITY_FILE_WEIGHT"
else
echo "complexity-score.sh: COMPLEXITY_FILE_WEIGHT='$COMPLEXITY_FILE_WEIGHT' is not a positive integer" >&2
exit 4
fi
fi

STDERR_TMP="$(mktemp)"
if ! META="$(gh pr view "$PR_NUM" --json additions,deletions,changedFiles 2>"$STDERR_TMP")"; then
if grep -qiE 'not.?found|could not resolve|no pull requests? found' "$STDERR_TMP"; then
rm -f "$STDERR_TMP"
echo "complexity-score.sh: PR #$PR_NUM not found" >&2
exit 3
fi
cat "$STDERR_TMP" >&2
rm -f "$STDERR_TMP"
exit 4
fi
rm -f "$STDERR_TMP"

ADD=$(printf '%s' "$META" | jq -r '.additions')
DEL=$(printf '%s' "$META" | jq -r '.deletions')
FILES=$(printf '%s' "$META" | jq -r '.changedFiles')
SCORE=$(( ADD + DEL + FILE_WEIGHT * FILES ))

if (( JSON_OUT )); then
jq -n \
--argjson score "$SCORE" \
--argjson additions "$ADD" \
--argjson deletions "$DEL" \
--argjson changedFiles "$FILES" \
--argjson fileWeight "$FILE_WEIGHT" \
'{
score: $score,
additions: $additions,
deletions: $deletions,
changedFiles: $changedFiles,
file_weight: $fileWeight,
formula: "additions + deletions + file_weight * changedFiles"
}'
else
printf '%s\n' "$SCORE"
fi
exit 0
Loading
Loading