agentleFS
xsovad06xsovad06/sova/.agents/skills/sova-pr/SKILL.md

sova-pr

Skill3 starsChanged 6 days agoApache-2.0View on GitHub

Create a pull request with the standard template, analyzing all commits and changes on the current branch.

  • Deletes or force-pushes
  • Commits and pushes

The file

Show its rules and text

We split this skill into the rules it gives an agent

41 rules. About 0 are common, 20 are distinctive, and 21 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈20

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 21

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  • could be automatic: a setting could enforce it
  1. Distinctive:

    Create a pull request for the current branch using the project's standard PR template.

    line 8
  2. Distinctive:

    If a PR already exists, ASK THE USER which workflow they want (when running headless, with no user to ask, use Option B: it never rewrites …

    line 38
  3. Distinctive:

    Option B: Incremental workflow: add new commits on top of the existing remote history.

    line 40
  4. Distinctive:

    Create a new feature branch from changes

    line 58
  5. Very specific:

    Use conventional commit format: type(scope): subject

    line 78
  6. Distinctive:

    Write multi-line commit messages explaining WHAT, WHY, and HOW

    line 79
  7. Very specific:

    Read the full diff: git diff "origin/$BASE"...HEAD (re-assign BASE as in step 1)

    line 125
  8. Distinctive:

    Look for migration files, API spec changes, test coverage

    line 127
  9. Very specific:

    Body: Use the project's PR template if one exists (check .github/PULL_REQUEST_TEMPLATE.md).

    line 133
  10. Distinctive:

    Do not include preamble or commentary before the first heading.

    line 150
  11. Very specific:

    Do NOT use Closes #N, Fixes #N, or Resolves #N (those are GitHub Issue syntax)

    line 157
  12. Very specific:

    Add a ## JIRA section at the bottom of the body with a link to the ticket: PROJECT_KEY-NUMBER

    line 158
  13. Very specific:

    If the arguments provided when this skill is invoked contains a GitHub Issue number (e.g., #42), fetch the issue body (gh issue view 42 …

    line 163
  14. Very specific:

    If every criterion is met by this PR (or the issue has no ## Acceptance Criteria section at all), add a ## Issue section containing Closes …

    line 164
  15. Very specific:

    Always place the decision under its own ## Issue heading, not as trailing text inside another section, so a later sova-address-pr skill …

    line 167
  16. Distinctive:

    Run preflight CI checks locally: before pushing: Run the full checks, skipping only when an identical check already passed earlier in this …

    line 178
  17. Very specific:

    Run the full checks, skipping only when an identical check already passed earlier in this autonomous pipeline cycle for this exact worktree …

    line 179
  18. Distinctive:

    Verify authorship and text: before pushing. Each check must print nothing.

    line 193
  19. Very specific:

    If it prints True and the PR was newly created (not an update), post a comment to trigger CodeRabbit.

    line 275
  20. Very specific:

    Wait for every check that gates the PR, not just one matched workflow run: gh run list only sees runs it triggered directly, so a check …

    line 291
  21. Very specific:

    Only when --required reports none and the block falls back to every check does it narrow them to the ruleset names, so a non-required check …

    line 325
  22. Very specific:

    CI PASSED: every check has bucket pass or skipping and no required check is missing.

    line 417
  23. Very specific:

    CI CANCELED: treat as a non-success state, not a pass.

    line 419
  24. Very specific:

    Run the sova-review-pr skill against this PR to review the actual diff that will be merged:

    line 426
  25. Distinctive:

    If the verdict has no findings >= 3/10: skip step 18, go to step 19

    line 430
  26. Distinctive:

    If there are findings >= 3/10: continue to step 18

    line 431
  27. Very specific:

    Run the sova-address-pr skill to fix the findings:

    line 434
  28. Distinctive:

    Wait for CI again (go back to step 15 logic, max 3 total CI cycles across the entire run)

    line 440
  29. Distinctive:

    Trigger CodeRabbit now: if step 12 was deferred, using step 12's block unchanged.

    line 442
  30. Very specific:

    Before this: Run the sova-review skill or the sova-review-full skill to catch issues before pushing

    line 457
  31. Very specific:

    After merge: Run the sova-integrate-pr skill for merge, cleanup, and knowledge extraction

    line 459
  32. Very specific:

    Need to reorganize commits first?: Run the sova-rearrange-commits skill

    line 460
  33. Distinctive:

    If the branch has no commits ahead of the base branch and no uncommitted changes, inform the user

    line 464
  34. Distinctive:

    All commits on the branch will be analyzed to generate the PR description

    line 465
  35. Distinctive:

    NEVER merge the PR yourself unless the user explicitly asks

    line 466
  36. Very specific:

    Use --force-with-lease for force pushes, never --force

    line 467
  37. Very specific:

    NEVER skip CI checks or use --no-verify

    line 468
  38. Distinctive:

    If CI fails 3 times, stop and ask the user for guidance

    line 469
  39. Distinctive:

    Do NOT ask for or request reviewers: the user handles reviews themselves

    line 470
  40. Distinctive:

    NEVER include AI references in commits or PRs

    line 471
  41. Distinctive:

    NEVER use emojis in any output

    line 472

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Create Pull Request

Create a pull request for the current branch using the project's standard PR template.

Instructions

  1. Resolve the base branch and fetch (do not rebase yet: step 3 decides whether rebasing is safe for this PR): Resolve the base branch at runtime rather than assuming main. Every shell block in this command runs in a fresh shell, so each block that uses BASE re-assigns it first. Fall back to the repo default only when gh pr view confirms there is no PR for this branch yet; abort on any other failure (auth, network, API) instead of guessing:
    BASE=$(gh pr view --json baseRefName --jq .baseRefName 2>&1) || { case "$BASE" in *"no pull requests found"*) BASE=$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name) ;; *) echo "Unable to resolve the base branch: $BASE" >&2; exit 1 ;; esac; }
    [ -n "$BASE" ] || { echo "Unable to resolve the base branch" >&2; exit 1; }
    git fetch origin || { echo "Fetch failed: STOP, origin/$BASE may be stale" >&2; exit 1; }
    echo "Base branch: $BASE"
    • Never check out the base branch: in a linked worktree it is usually checked out elsewhere, so git checkout "$BASE" fails. Rebase onto the remote-tracking ref instead.
    • Verify the GitHub account: gh auth status must show the account the project configures as its GitHub user (for SOVA projects, sova config get github_user prints it). Otherwise --assignee @me in step 11 assigns the wrong user and later thread writes fail with FORBIDDEN. If it differs, run gh auth switch --user <github_user>.
  2. Gather branch information. These commands do not need $BASE and can run in parallel:

    • git status: check for uncommitted changes
    • git branch --show-current: get current branch name
    • git diff --stat: summary of uncommitted changes
    • gh pr list --head $(git branch --show-current) --json number,url,state: check if PR already exists

    The two commands below need $BASE, resolved fresh in this shell (same fail-closed resolution as step 1):

    BASE=$(gh pr view --json baseRefName --jq .baseRefName 2>&1) || { case "$BASE" in *"no pull requests found"*) BASE=$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name) ;; *) echo "Unable to resolve the base branch: $BASE" >&2; exit 1 ;; esac; }
    [ -n "$BASE" ] || { echo "Unable to resolve the base branch" >&2; exit 1; }
    git log "origin/$BASE"..HEAD --oneline
    git diff "origin/$BASE"...HEAD --stat
  3. Handle existing PR, then rebase only when it is safe: If a PR already exists, ASK THE USER which workflow they want (when running headless, with no user to ask, use Option B: it never rewrites history the remote already has):

    • Option A: AI feedback workflow: reset and recreate all commits (back up the branch, soft reset to the fork point, reorganize, verify: see step 5). Rebasing is safe here since step 10 force-pushes this option.
    • Option B: Incremental workflow: add new commits on top of the existing remote history. Do NOT rebase: step 10 pushes this option with a plain git push, which fails as non-fast-forward if the rebase rewrote commit IDs the remote already has.

    For a new PR (no existing PR yet) or Option A, rebase now, before adding any commits:

    • Never use git stash: the stash stack is shared by every worktree and session of this repository, so a pop can restore someone else's changes. If git status --porcelain prints anything, review it first (nothing that must not be committed, such as secrets or scratch files), then set the work aside as a WIP commit; step 5 turns it into real commits:
      git add -A && git commit -m "chore: WIP snapshot before rebase" \
        || { echo "WIP commit failed (hook, signing or staging error): STOP, nothing was reset" >&2; exit 1; }
      [ -z "$(git status --porcelain)" ] || { echo "Uncommitted changes remain after the WIP commit: STOP before rebasing" >&2; exit 1; }
    • Rebase:
      BASE=$(gh pr view --json baseRefName --jq .baseRefName 2>&1) || { case "$BASE" in *"no pull requests found"*) BASE=$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name) ;; *) echo "Unable to resolve the base branch: $BASE" >&2; exit 1 ;; esac; }
      [ -n "$BASE" ] || { echo "Unable to resolve the base branch" >&2; exit 1; }
      git rebase "origin/$BASE"
    • Handle rebase conflicts (if any): read each conflicted file, resolve the markers, git add <file>, then git rebase --continue. If a conflict cannot be resolved with confidence, run git rebase --abort, then undo the WIP commit if one was made (git log -1 --format=%s prints chore: WIP snapshot before rebase: run git reset --soft HEAD~1), report the conflicting files to the user, and stop.
  4. Handle the base branch (if the current branch is the base branch itself):
    • Create a new feature branch from changes
    • Generate a descriptive branch name (e.g., fix/lint-errors, feat/add-caching)
    • Continue PR process on the new branch
  5. Recreate history (Option A) and handle uncommitted changes. For Option A, the first bullet and the verification after the commit format below always run, even when nothing is uncommitted. The remaining bullets apply whenever uncommitted changes exist, including a WIP commit from step 3:

    • Option A only: recreate every commit. Back up the branch, then soft-reset to the fork point; this replaces the WIP undo in the next bullet, since the reset uncommits the WIP snapshot too. Never reset to origin/$BASE by name: the step 3 rebase made the two equal, but a later fetch can move origin/$BASE, and recommitting this branch's tree on top of the newer base silently reverts everything the base gained. Note the printed backup name: the verification below and step 10 re-assign BACKUP to it.
      [ -z "$(git status --porcelain)" ] || { echo "Uncommitted changes remain: STOP before creating the backup" >&2; exit 1; }
      BASE=$(gh pr view --json baseRefName --jq .baseRefName 2>&1) || { case "$BASE" in *"no pull requests found"*) BASE=$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name) ;; *) echo "Unable to resolve the base branch: $BASE" >&2; exit 1 ;; esac; }
      [ -n "$BASE" ] || { echo "Unable to resolve the base branch" >&2; exit 1; }
      FORK_POINT=$(git merge-base HEAD "origin/$BASE") || { echo "Cannot resolve the fork point: STOP before resetting" >&2; exit 1; }
      BACKUP="backup/pr-$(date +%s)-$$-$RANDOM"
      git branch "$BACKUP" || { echo "Failed to create backup branch $BACKUP: STOP before resetting" >&2; exit 1; }
      echo "Backup branch: $BACKUP"
      git reset --soft "$FORK_POINT" && git reset HEAD .
    • Otherwise (a new PR), if step 3 created a WIP commit, confirm git log -1 --format=%s prints chore: WIP snapshot before rebase, then undo it with git reset --soft HEAD~1 so its contents are uncommitted again
    • Analyze and group related changes logically
    • Create small, atomic commits with detailed explanations:
      • Each commit = ONE logical change
      • Use conventional commit format: type(scope): subject
      • Write multi-line commit messages explaining WHAT, WHY, and HOW

    Commit message format:

    type(scope): short description (max 50 chars)
    
    Detailed explanation of what this commit does and why.
    Include before/after behavior if applicable.

    Option A only: verify the recreated history. Both commands below must print nothing: the recreated branch must have exactly the tree it had before.

    BACKUP=<backup branch printed in step 5>
    git status --porcelain
    git diff "$BACKUP"

    Then confirm the recreated history kept what the base gained. The two checks above cannot see this: a reset that wrongly targeted origin/$BASE leaves the tree identical, so git diff "$BACKUP" stays empty while the branch's diff now reverts everything the base gained since the fork point. The block must end by printing File-set check passed.

    BASE=$(gh pr view --json baseRefName --jq .baseRefName 2>&1) || { case "$BASE" in *"no pull requests found"*) BASE=$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name) ;; *) echo "Unable to resolve the base branch: $BASE" >&2; exit 1 ;; esac; }
    [ -n "$BASE" ] || { echo "Unable to resolve the base branch" >&2; exit 1; }
    BACKUP=<backup branch printed in step 5>
    OLD_FORK=$(git merge-base "$BACKUP" "origin/$BASE") || { echo "Cannot resolve the backup's fork point: STOP" >&2; exit 1; }
    NEW_FORK=$(git merge-base HEAD "origin/$BASE") || { echo "Cannot resolve HEAD's fork point: STOP" >&2; exit 1; }
    BEFORE=$(git diff --name-only "$OLD_FORK" "$BACKUP") || { echo "git diff failed: STOP" >&2; exit 1; }
    AFTER=$(git diff --name-only "$NEW_FORK" HEAD) || { echo "git diff failed: STOP" >&2; exit 1; }
    BASE_CHANGED=$(git diff --name-only "$OLD_FORK" "origin/$BASE") || { echo "git diff failed: STOP" >&2; exit 1; }
    # A file the squash newly brought into the branch's diff that the base changed since the fork is a reverted base change.
    REVERTED=$(comm -12 <(comm -13 <(sort <<<"$BEFORE") <(sort <<<"$AFTER")) <(sort <<<"$BASE_CHANGED"))
    if [ -n "$REVERTED" ]; then printf 'Squash reverted base changes in:\n%s\nSTOP\n' "$REVERTED" >&2; exit 1; fi
    echo "File-set check passed"

    If any check fails, first preserve anything uncommitted: git status --porcelain output means tracked or untracked edits exist only in the working tree, made during commit recreation, and are not in $BACKUP or any commit on this branch, so the recovery reset below would silently discard them. Commit them to a rescue branch before resetting, then redo Option A's reset from the top of this step:

    BACKUP=<backup branch printed in step 5>
    if [ -n "$(git status --porcelain)" ]; then
      git add -A && git commit -m "chore: WIP rescue before re-reset" \
        || { echo "Rescue commit failed: STOP, do not reset over uninspected changes" >&2; exit 1; }
      RESCUE="rescue/squash-$(date +%s)-$$-$RANDOM"
      git branch "$RESCUE" || { echo "Failed to create rescue branch $RESCUE: STOP" >&2; exit 1; }
      echo "Rescue branch: $RESCUE"
    fi
    git reset --hard "$BACKUP"
  6. Analyze all changes (existing + new commits):
    • Read the full diff: git diff "origin/$BASE"...HEAD (re-assign BASE as in step 1)
    • Identify the type of change (feature, bugfix, refactor, etc.)
    • Look for migration files, API spec changes, test coverage
  7. Generate PR content:

    Title: Concise, descriptive title based on the changes.

    Body: Use the project's PR template if one exists (check .github/PULL_REQUEST_TEMPLATE.md). Otherwise use:

    ## Summary
    3-5 bullet points: WHAT changed and WHY, folding in the file-by-file
    breakdown (which files/areas each bullet touches) rather than repeating it
    in a separate section.
    
    ## Review guidance
    What should a reviewer focus on? Any trade-offs or shortcuts?
    
    ## Test plan
    Name the specific test file(s) (and test names, where relevant) that cover
    this change, and how they were run. Do not write a generic statement like
    "tests were run" without naming which ones.

    Do not include preamble or commentary before the first heading. Do not use emojis.

    Link the issue in the PR body based on the task source:

    Determine the task source by running sova config and reading the task_source row (configuration lives in .claude/sova.db, not in a file).

    JIRA (type = "jira"):

    • Do NOT use Closes #N, Fixes #N, or Resolves #N (those are GitHub Issue syntax)
    • Add a ## JIRA section at the bottom of the body with a link to the ticket: [PROJECT_KEY-NUMBER](https://your-jira-instance/browse/PROJECT_KEY-NUMBER)
    • The PR title MUST start with [PROJECT_KEY-NUMBER] followed by a human-readable description (NOT the branch name)

    GitHub (default):

    • If the arguments provided when this skill is invoked contains a GitHub Issue number (e.g., #42), fetch the issue body (gh issue view 42 --json body) and check its ## Acceptance Criteria section against the actual diff on this branch:
      • If every criterion is met by this PR (or the issue has no ## Acceptance Criteria section at all), add a ## Issue section containing Closes #42.
      • Count a criterion that matches one of the project's CI-gated patterns (sova config get commit.ci_gated_patterns, plain case-insensitive substrings; by default the make check and make lint criteria) as met: the merge gate settles it, and the closing keyword only acts at merge. An owner/interactive/manual-only criterion stays unmet even when it also matches.
      • If any criterion is unmet, marked owner/interactive/manual-only, or has an unknown/unparseable verdict (you cannot tell from the diff whether it was met), do NOT use Closes #42: add a ## Issue section containing Part of #42, a blank line, Not done in this PR:, and a bullet for each unmet, owner-only, or unknown criterion instead. Reserve Closes #42 for when every criterion is verified as met or the issue has no acceptance criteria at all. This mirrors the autonomous pipeline's decision (sova/core/steps/_pr_body.py); the two must agree.
      • Always place the decision under its own ## Issue heading, not as trailing text inside another section, so a later sova-address-pr skill round can find and replace it without disturbing other sections.

    Save the body to a file so step 9 can check it and step 11 can submit it unchanged. Note the printed path: later shell blocks are fresh shells and must re-assign BODY_FILE to it.

    BODY_FILE=$(mktemp)
    cat > "$BODY_FILE" <<'EOF'
    [THE BODY]
    EOF
    echo "PR body saved to $BODY_FILE"
  8. Run preflight CI checks locally before pushing: Run the full checks, skipping only when an identical check already passed earlier in this autonomous pipeline cycle for this exact worktree state (a no-op outside the autonomous pipeline: sova checks should-skip only ever reports a skip there):

    if sova checks should-skip "make check"; then
      echo "Full check already passed for this worktree state earlier in the pipeline cycle; skipping re-run."
    else
      make check && { sova checks record "make check" --passed || true; }
    fi

    It is only as complete as the project configured it: if it is not the same target CI runs, also run the CI steps it omits (read the CI workflow file to list them). This must pass before any push. Fix any failures before proceeding.

    If the project has pre-push invariant scripts (invariants/*.sh), run every one against the base (for s in invariants/*.sh; do bash "$s" . "$BASE" || echo "FAIL: $s"; done, with BASE resolved as in step 1) and fix every failure: the pre-push hook runs the same scripts, and a push it blocks is fixed by correcting the reported problem, never by --no-verify.

    If the project has an executable .claude/scripts/prepush-check.sh, run it too. It holds project-specific mechanical checks (deploy config drift, dependency downgrades after a rebase, spec regeneration) that generic commands cannot know about. Treat every FAIL line as blocking and every WARN line as something to confirm before pushing.

  9. Verify authorship and text before pushing. Each check must print nothing; fix any hit (reword the commit, correct the author, or edit the body file) and re-run all of them:

    BASE=$(gh pr view --json baseRefName --jq .baseRefName 2>&1) || { case "$BASE" in *"no pull requests found"*) BASE=$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name) ;; *) echo "Unable to resolve the base branch: $BASE" >&2; exit 1 ;; esac; }
    [ -n "$BASE" ] || { echo "Unable to resolve the base branch" >&2; exit 1; }
    BODY_FILE=<path printed in step 7>
    # No AI attribution: co-author trailers, "generated with" lines, or an AI author/committer identity.
    # Match attribution only; a body that merely mentions a `.claude/` path or CLAUDE.md is fine.
    git log "origin/$BASE"..HEAD --format='%an <%ae>%n%cn <%ce>%n%b' \
      | grep -iE '^co-authored-by:|generated with claude|noreply@anthropic\.com|^claude <'
    # Every commit is authored and committed by the configured git identity
    git log "origin/$BASE"..HEAD --format='%ae%n%ce' | sort -u | grep -vxF "$(git config user.email)"
    # No " -- " used as punctuation in commit messages or the PR body
    git log "origin/$BASE"..HEAD --format='%s%n%b' | grep -nF ' -- '
    grep -nF ' -- ' "$BODY_FILE"

    A hit inside a fenced code sample in the body (a literal end-of-options separator) is acceptable; a hit in prose is not: use a colon, a comma, or a single hyphen instead.

    For a GitHub issue, also stop if the body would close an issue other than the intended one. A quoted Closes #12 in prose closes #12 on merge. Set ISSUE to the issue number from the arguments provided when this skill is invoked (empty when there is none: step 8 then writes no closing link, so any closing reference in the body is stray and stops the check). The check prints nothing when the body is safe; a printed STOP line (exit 1) is a hard stop: reword the body file (put the phrase in a code span, or say "stays open"), then re-run it before pushing. A SKIP line on stderr (exit 2) means python3 cannot import sova (an active virtualenv hides the user-site install), so nothing was checked: run the same script with the interpreter sova is installed under, or read the body file yourself for any Closes, Fixes or Resolves followed by an issue number other than ISSUE, and treat a hit as the same hard stop.

    BODY_FILE=<path printed in step 7>
    ISSUE=<issue number from the arguments, digits only, or empty>
    python3 - "$BODY_FILE" "$ISSUE" <<'PY'
    import sys
    from pathlib import Path
    
    body_file, issue = sys.argv[1], sys.argv[2].lstrip("#")
    try:
        from sova.utils.gh import parse_issue_links
    except ImportError:
        print("SKIP: sova is not importable from python3, the closing-reference check did not run", file=sys.stderr)
        sys.exit(2)
    
    body = Path(body_file).read_text(encoding="utf-8")
    extra = [n for n in parse_issue_links(body).closes if n != issue]
    if extra:
        closed = ", ".join("#" + n for n in extra)
        if issue:
            print(f"STOP: the PR body would close {closed}, not only #{issue}")
        else:
            print(f"STOP: the PR body would close {closed}, but no issue was given")
        sys.exit(1)
    PY
  10. Push the branch:
    • New PR: git push -u origin $(git branch --show-current)
    • Existing PR (AI feedback): push, then delete the step 5 backup only once the push has succeeded. If the push fails, keep the backup and report its name:
      BACKUP=<backup branch printed in step 5>
      git push --force-with-lease origin "$(git branch --show-current)" && git branch -D "$BACKUP"
    • Existing PR (incremental): git push origin $(git branch --show-current)
  11. Create or update PR:
    BODY_FILE=<path printed in step 7>
    # New PR (--assignee @me ensures creator is always assigned)
    gh pr create --assignee @me --title "THE_TITLE" --body-file "$BODY_FILE" \
      || { echo "PR create failed: STOP" >&2; exit 1; }
    
    # Update existing PR
    gh pr edit <PR_NUMBER> --title "THE_TITLE" --body-file "$BODY_FILE" \
      || { echo "PR edit failed: STOP" >&2; exit 1; }
    
    # Once the PR is created or updated, remove the temporary body file
    rm -f "$BODY_FILE"
    
    # Move linked issues to in review (GitHub only); a worktree has no sova.db
    PRIMARY=$(cd "$(git rev-parse --git-common-dir)/.." && pwd)
    if [ "$(sova config get task_source.type --project "$PRIMARY")" = github ]; then
      REPO=$(sova config get github_repo --project "$PRIMARY")
      test -n "$REPO" || { echo "STOP: github_repo is unset" >&2; exit 1; }
      PR_NUM=$(gh pr view "$(git branch --show-current)" --repo "$REPO" --json number --jq .number) && sova linked-issues "$PR_NUM" --mark-in-review
    fi
  12. Trigger CodeRabbit review (if configured): only once the code it should review is final. When steps 14-18 will run (they push again), defer this step until step 19, after the last push: a review of pre-fix code spends the bot's limited quota and goes stale. This key is not in the sova config table, so resolve it through the config loader:
    python3 -c "from pathlib import Path; from sova.config.loader import load_config; \

print(load_config(Path('.')).external_reviews.coderabbit.trigger_review)" If it prints `True` and the PR was newly created (not an update), post a comment to trigger CodeRabbit. This is a fresh shell, so resolve the PR number again: bash PR_NUM=$(gh pr view --json number --jq .number) || { echo "Unable to resolve the PR number" >&2; exit 1; } gh pr comment "$PR_NUM" --body "@coderabbitai review" ```

  1. Return the PR URL to the user.

Post-Push: CI + Self-Review

These phases run after the PR is created/updated. They enable autonomous operation (push-to-ready-to-merge in one command).

  1. Visual verification (if applicable): If the project has a local verification procedure AND changes affect UI (templates, CSS, JS, views, but not test-only or migration-only), follow that procedure. If verification reveals issues, fix them, commit the fix (amend into the relevant commit or add a new commit), and push (git push --force-with-lease) before proceeding to step 15: otherwise CI runs against the old commit and step 17 reviews a diff that doesn't contain the fix. Skip if no such procedure exists.
  2. Wait for CI pipeline: Wait for every check that gates the PR, not just one matched workflow run: gh run list only sees runs it triggered directly, so a check that arrives as a mirrored commit status (e.g. SonarCloud, which posts via .github/workflows/sonarcloud.yml after CI's coverage artifact lands) or from any other separate workflow can still be pending, or can later fail, after the one run gh run list matched already passed. gh pr checks is scoped to the PR's current head by GitHub itself, so it needs no SHA matching and cannot return a stale or unrelated run.

    The wait covers the required checks when the base branch has any, and every check on the PR head when it has none: a repository without branch protection (e.g. a private repository on a plan that offers none) has no required checks, so gh pr checks --required prints no required checks reported and exits 1 on every call. gh also exits non-zero while a check is pending (8) or failing (1) and still prints the JSON then, so the block decides from gh's message, never from the exit code alone. Right after a push GitHub may not have registered any check yet (no checks reported): that is a reason to keep polling, and an empty check list is never a pass. While the head has no check, the block also lists the workflow runs for the head commit. A head with neither a check nor a run after about 2 minutes (5 consecutive polls; registering normally takes seconds) never triggered CI, most often because the workflows' path filters exclude every changed file (e.g. a docs-only change), so the block stops there instead of waiting out the budget. A workflow file error or an exhausted Actions budget still creates a (failed) run, so it never ends that way.

    gh pr checks --required lists only the required checks registered so far, so a partial green list could pass while a required one (e.g. SonarCloud) has not appeared. The block reads the base branch's required names once from the rulesets (gh api --paginate repos/{owner}/{repo}/rules/branches/<base>, every page) and passes only when none is missing. Every row --required returns decides, even one the rulesets do not list (classic protection requires it too), so a failing classic-required check is never ignored. Only when --required reports none and the block falls back to every check does it narrow them to the ruleset names, so a non-required check such as a review bot neither fails nor blocks the pass. Unregistered names are listed in the final CI PENDING verdict and a failed lookup stops the block. Classic branch protection is not consulted for names: the rulesets endpoint reports ruleset rules only, and the classic endpoint needs the admin right, which a push-only account lacks. A repository that relies on classic protection alone (or has no rulesets, also a private repository whose plan offers none) therefore reads as no required names and keeps the behavior above. The block polls every 30 seconds, at most 16 times, and ends by printing one verdict line:

    PR_NUM=$(gh pr view --json number --jq .number) || { echo "Unable to resolve the PR number" >&2; exit 1; }
    ERR_FILE=$(mktemp) || { echo "Unable to create a temp file: STOP" >&2; exit 1; }
    trap 'rm -f "$ERR_FILE"' EXIT
    BASE=$(gh pr view "$PR_NUM" --json baseRefName --jq .baseRefName) || { echo "Unable to resolve the PR base branch" >&2; exit 1; }
    # Rulesets, every page (--paginate prints one JSON array per page); a plan without rulesets is no rules.
    RULES_JSON=$(gh api --paginate "repos/{owner}/{repo}/rules/branches/$BASE?per_page=100" 2>"$ERR_FILE") \
      || { [[ $RULES_JSON$(cat "$ERR_FILE") == *"Upgrade to GitHub Pro"* ]] && RULES_JSON="[]"; } \
      || { echo "Unable to read the required checks of $BASE: $(cat "$ERR_FILE")" >&2; exit 1; }
    REQUIRED_JSON=$(jq -sce '
      if length == 0 or any(.[]; type != "array") then error("not a rule list") else
      [.[][] | select(.type == "required_status_checks") | (.parameters.required_status_checks // [])[].context] | unique end' \
      <<<"$RULES_JSON" 2>/dev/null) || { echo "Unable to parse the required checks of $BASE" >&2; exit 1; }
    NO_CI_POLLS=0
    for i in $(seq 1 16); do
      SCOPE=required
      FALLBACK=0
      CHECKS_JSON=$(gh pr checks "$PR_NUM" --required --json name,bucket,link 2>"$ERR_FILE")
      if grep -q "no required checks reported" "$ERR_FILE"; then
        SCOPE=all
        FALLBACK=1
        CHECKS_JSON=$(gh pr checks "$PR_NUM" --json name,bucket,link 2>"$ERR_FILE")
      fi
      grep -q "no checks reported" "$ERR_FILE" && CHECKS_JSON="[]"
      # With required names, only those decide when the rows are every check of the head (the fallback): a
      # non-required check never fails or blocks the pass. Rows from --required are all required (a ruleset or
      # classic protection), so every one of them decides, listed in the rulesets or not.
      [ "$REQUIRED_JSON" = "[]" ] || SCOPE=required
      # TOTAL, PENDING, NOT_PASSED (every bucket that is neither pass, skipping, pending nor cancel), CANCELED,
      # then ALL_TOTAL: every check on the head, required or not.
      STATS=$(jq -r --argjson req "$REQUIRED_JSON" --argjson fb "$FALLBACK" 'if type != "array" then error("not a check list") else
        (if $fb == 0 or ($req | length) == 0 then . else map(select(.name as $n | any($req[]; . == $n))) end) as $c
        | [($c | length), ($c | map(select(.bucket == "pending")) | length),
           ($c | map(select(.bucket != "pass" and .bucket != "skipping" and .bucket != "pending" and .bucket != "cancel")) | length),
           ($c | map(select(.bucket == "cancel")) | length), length] | @tsv end' <<<"$CHECKS_JSON" 2>/dev/null) && [ -n "$STATS" ] \
        || { echo "Unable to read the PR's checks: $(cat "$ERR_FILE")" >&2; exit 1; }
      IFS=$'\t' read -r TOTAL PENDING NOT_PASSED CANCELED ALL_TOTAL <<<"$STATS"
      jq -r '.[] | "\(.bucket)\t\(.name)\t\(.link)"' <<<"$CHECKS_JSON"
      # Required names absent from the PR's checks (compared inside jq).
      MISSING_JSON=$(jq -c --argjson req "$REQUIRED_JSON" '$req - map(.name)' <<<"$CHECKS_JSON") \
        || { echo "Unable to compare the required checks with the PR's checks" >&2; exit 1; }
      MISSING=$(jq 'length' <<<"$MISSING_JSON")
      jq -r '.[] | "missing\t\(.)"' <<<"$MISSING_JSON"
      if [ "$TOTAL" -gt 0 ] && [ "$PENDING" -eq 0 ]; then
        VERDICT=
        if [ "$NOT_PASSED" -gt 0 ]; then VERDICT="CI FAILED ($SCOPE checks): $NOT_PASSED of $TOTAL did not pass"
        elif [ "$CANCELED" -gt 0 ]; then VERDICT="CI CANCELED ($SCOPE checks): $CANCELED of $TOTAL canceled"
        elif [ "$MISSING" -eq 0 ]; then VERDICT="CI PASSED ($SCOPE checks): all $TOTAL passed or were skipped"; fi
        [ -z "$VERDICT" ] || { echo "$VERDICT"; exit 0; }
      fi
      NO_CI_POLLS_BEFORE=$NO_CI_POLLS
      NO_CI_POLLS=0
      if [ "$ALL_TOTAL" -eq 0 ]; then
        # No check of any kind. Count only polls that confirmed zero runs; a failed lookup proves nothing and restarts the grace.
        RUNS=unknown
        HEAD_SHA=$(gh pr view "$PR_NUM" --json headRefOid --jq .headRefOid 2>/dev/null) \
          && RUNS_JSON=$(gh run list --commit "$HEAD_SHA" --json status,conclusion,workflowName 2>/dev/null) \
          && RUNS=$(jq -er 'length' <<<"$RUNS_JSON" 2>/dev/null) || RUNS=unknown
        if [ "$RUNS" = unknown ]; then echo "Unable to list the workflow runs for the PR head"
        elif [ "$RUNS" -eq 0 ]; then NO_CI_POLLS=$((NO_CI_POLLS_BEFORE + 1))
        else jq -r '.[] | "run\t\(.status)\t\(.conclusion)\t\(.workflowName)"' <<<"$RUNS_JSON"; fi
        if [ "$NO_CI_POLLS" -ge 5 ]; then
          echo "NO CI TRIGGERED: no check and no workflow run for head $HEAD_SHA after 2 minutes"
          exit 0
        fi
      fi
      [ "$i" -lt 16 ] && sleep 30
    done
    if [ "$ALL_TOTAL" -eq 0 ]; then echo "NO CHECKS: none reported on the PR head, though a workflow run exists for it or could not be listed"
    else
      MSG="CI PENDING ($SCOPE checks): $PENDING of $TOTAL still pending"
      [ "$MISSING" -eq 0 ] || MSG="$MSG; $MISSING required not registered: $(jq -r 'join(", ")' <<<"$MISSING_JSON")"
      echo "$MSG"
    fi

    Max wait: 30 minutes in total (a full test suite routinely runs longer than 15).

    Run each wait as a synchronous foreground command, never as a background task: in headless mode nothing resumes you when a background task finishes, and ending a turn without a tool call ends the run. The Bash tool caps a single call at 10 minutes (600000 ms) and defaults to 2, so pass an explicit timeout of at least 540000 ms on every call of the block above; its 16 polls of 30 seconds keep one call to about 8 minutes. When a call ends with CI PENDING or NO CHECKS, run the same block again as a new call, until it prints another verdict or the 30 minute total is used up. The block re-resolves PR_NUM itself, since each call is a fresh shell.

  3. Handle CI result (the verdict line of the last step 15 call; "checks" below means the scope it names: the required checks, or every check when the repository requires none):
    • CI PASSED: every check has bucket pass or skipping and no required check is missing; continue to step 17 (a skipping check, e.g. a path-filtered workflow, is treated as satisfied).
    • CI FAILED: fetch logs for each check listed with a bucket other than pass, skipping or cancel (gh run view <run_id> --log-failed when link points at a GitHub Actions run, otherwise open link), analyze, fix, amend to the relevant commit, re-run step 9's checks, force push (--force-with-lease), go back to step 15 (max 3 CI retry cycles total). If still failing after 3 attempts, stop and ask the user.
    • CI CANCELED: treat as a non-success state, not a pass. Report it and re-run step 15 to trigger it again (e.g. re-push or gh run rerun) before continuing; do not proceed to step 17 with a canceled check.
    • CI PENDING once the 30 minute total is used up: report the current statuses and the PR URL, naming any required check the verdict lists as not registered. Suggest the user check back later.
    • NO CI TRIGGERED: no CI ran for this head commit, most likely because the workflows' path filters exclude every changed file. The step 9 local checks are the only verification this change gets. Say exactly that in the report (and in the PR when you comment on CI), never "CI passed", then continue to step 17.
    • NO CHECKS once the 30 minute total is used up: a workflow run exists for the head (or could not be listed) but no check ever registered on it. Report the runs the block listed and the PR URL. Do not continue to step 17: an empty check list is never a pass.
    • The block stops with Unable to (read the PR's checks, resolve the base branch, read or parse its required checks): a gh or network failure, not a CI result. Run the block again; if it fails the same way twice, stop and report the error.
  4. Self-review the PR diff: Run the sova-review-pr skill against this PR to review the actual diff that will be merged:
    1. Fetch the PR number from the branch
    2. Execute the sova-review-pr skill's analysis in full (fetch diff, read files, deep analysis)
    3. Post the review on GitHub
    4. If the verdict has no findings >= 3/10: skip step 18, go to step 19
    5. If there are findings >= 3/10: continue to step 18
  5. Address review findings: Run the sova-address-pr skill to fix the findings:
    1. Score and address each finding (fix or acknowledge)
    2. Commit fixes
    3. Reply to review comments on GitHub
    4. Resolve threads
    5. Re-run step 9's checks, then force push: git push --force-with-lease
    6. Wait for CI again (go back to step 15 logic, max 3 total CI cycles across the entire run)
  6. Trigger CodeRabbit now if step 12 was deferred, using step 12's block unchanged. Do the same before stopping early at step 16 (CI still pending, or the retry limit reached), so a deferred trigger is never dropped.
  7. Report:
    ## PR Summary
    
    Branch: <branch>
    PR: <url>
    CI: passed (attempt N)
    Review: approved / N findings addressed
    Status: ready for /integrate-pr

Cross-References

  • Before this: Run the sova-review skill or the sova-review-full skill to catch issues before pushing
  • Full workflow: the sova-develop-full skill -> the sova-review-full skill -> the sova-pr skill -> the sova-integrate-pr skill
  • After merge: Run the sova-integrate-pr skill for merge, cleanup, and knowledge extraction
  • Need to reorganize commits first? Run the sova-rearrange-commits skill

Rules

  • If the branch has no commits ahead of the base branch and no uncommitted changes, inform the user
  • All commits on the branch will be analyzed to generate the PR description
  • NEVER merge the PR yourself unless the user explicitly asks
  • Use --force-with-lease for force pushes, never --force
  • NEVER skip CI checks or use --no-verify
  • If CI fails 3 times, stop and ask the user for guidance
  • Do NOT ask for or request reviewers: the user handles reviews themselves
  • NEVER include AI references in commits or PRs
  • NEVER use emojis in any output

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.