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
In this repository44 other files it gives its agents.
- AGENTS.mdAGENTS.md
- templates/AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- templates/CLAUDE.mdCLAUDE.md
- dashboard-designSkill
- database-patternsSkill
- design-systemSkill
- design-tasteSkill
- issue-templateSkill
- sova-address-prSkill
- sova-after-mergeSkill
- sova-agent-readinessSkill
- sova-audit-instructionsSkill
- sova-debugSkill
- sova-develop-explainSkill
- sova-develop-fullSkill
- sova-developSkill
- sova-extract-knowledgeSkill
- sova-find-taskSkill
- sova-health-auditSkill
- sova-ingest-reviewSkill
- sova-integrate-prSkill
- sova-issueSkill
- sova-new-featureSkill
- sova-optimize-knowledgeSkill
- sova-rearrange-commitsSkill
- sova-researchSkill
- sova-review-fullSkill
- sova-review-prSkill
- sova-reviewSkill
- sova-simplifySkill
- sova-specSkill
- sova-standupSkill
- sova-statusSkill
- sova-testSkill
- sova-update-docsSkill
- testing-patternsSkill
- visual-auditSkill
- dashboard-designSkill
- database-patternsSkill
- design-tasteSkill
- issue-templateSkill
- testing-patternsSkill
- visual-auditSkill
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
- Distinctive: line 8
Create a pull request for the current branch using the project's standard PR template.
- Distinctive: line 38
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 …
- Distinctive: line 40
Option B: Incremental workflow: add new commits on top of the existing remote history.
- Distinctive: line 58
Create a new feature branch from changes
- Very specific: line 78
Use conventional commit format: type(scope): subject
- Distinctive: line 79
Write multi-line commit messages explaining WHAT, WHY, and HOW
- Very specific: line 125
Read the full diff: git diff "origin/$BASE"...HEAD (re-assign BASE as in step 1)
- Distinctive: line 127
Look for migration files, API spec changes, test coverage
- Very specific: line 133
Body: Use the project's PR template if one exists (check .github/PULL_REQUEST_TEMPLATE.md).
- Distinctive: line 150
Do not include preamble or commentary before the first heading.
- Very specific: line 157
Do NOT use Closes #N, Fixes #N, or Resolves #N (those are GitHub Issue syntax)
- Very specific: line 158
Add a ## JIRA section at the bottom of the body with a link to the ticket: PROJECT_KEY-NUMBER
- Very specific: line 163
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 …
- Very specific: line 164
If every criterion is met by this PR (or the issue has no ## Acceptance Criteria section at all), add a ## Issue section containing Closes …
- Very specific: line 167
Always place the decision under its own ## Issue heading, not as trailing text inside another section, so a later sova-address-pr skill …
- Distinctive: line 178
Run preflight CI checks locally: before pushing: Run the full checks, skipping only when an identical check already passed earlier in this …
- Very specific: line 179
Run the full checks, skipping only when an identical check already passed earlier in this autonomous pipeline cycle for this exact worktree …
- Distinctive: line 193
Verify authorship and text: before pushing. Each check must print nothing.
- Very specific: line 275
If it prints True and the PR was newly created (not an update), post a comment to trigger CodeRabbit.
- Very specific: line 291
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 …
- Very specific: line 325
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 …
- Very specific: line 417
CI PASSED: every check has bucket pass or skipping and no required check is missing. - Very specific: line 419
CI CANCELED: treat as a non-success state, not a pass. - Very specific: line 426
Run the sova-review-pr skill against this PR to review the actual diff that will be merged:
- Distinctive: line 430
If the verdict has no findings >= 3/10: skip step 18, go to step 19
- Distinctive: line 431
If there are findings >= 3/10: continue to step 18
- Very specific: line 434
Run the sova-address-pr skill to fix the findings:
- Distinctive: line 440
Wait for CI again (go back to step 15 logic, max 3 total CI cycles across the entire run)
- Distinctive: line 442
Trigger CodeRabbit now: if step 12 was deferred, using step 12's block unchanged.
- Very specific: line 457
Before this: Run the sova-review skill or the sova-review-full skill to catch issues before pushing
- Very specific: line 459
After merge: Run the sova-integrate-pr skill for merge, cleanup, and knowledge extraction
- Very specific: line 460
Need to reorganize commits first?: Run the sova-rearrange-commits skill
- Distinctive: line 464
If the branch has no commits ahead of the base branch and no uncommitted changes, inform the user
- Distinctive: line 465
All commits on the branch will be analyzed to generate the PR description
- Distinctive: line 466
NEVER merge the PR yourself unless the user explicitly asks
- Very specific: line 467
Use --force-with-lease for force pushes, never --force
- Very specific: line 468
NEVER skip CI checks or use --no-verify
- Distinctive: line 469
If CI fails 3 times, stop and ask the user for guidance
- Distinctive: line 470
Do NOT ask for or request reviewers: the user handles reviews themselves
- Distinctive: line 471
NEVER include AI references in commits or PRs
- Distinctive: line 472
NEVER use emojis in any output
≈ “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.
Files
Create Pull Request
Create a pull request for the current branch using the project's standard PR template.
Instructions
- 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 usesBASEre-assigns it first. Fall back to the repo default only whengh pr viewconfirms 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 statusmust show the account the project configures as its GitHub user (for SOVA projects,sova config get github_userprints it). Otherwise--assignee @mein step 11 assigns the wrong user and later thread writes fail with FORBIDDEN. If it differs, rungh auth switch --user <github_user>.
- Never check out the base branch: in a linked worktree it is usually checked out elsewhere, so
Gather branch information. These commands do not need
$BASEand can run in parallel:git status: check for uncommitted changesgit branch --show-current: get current branch namegit diff --stat: summary of uncommitted changesgh 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 --statHandle 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. Ifgit status --porcelainprints 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>, thengit rebase --continue. If a conflict cannot be resolved with confidence, rungit rebase --abort, then undo the WIP commit if one was made (git log -1 --format=%sprintschore: WIP snapshot before rebase: rungit reset --soft HEAD~1), report the conflicting files to the user, and stop.
- 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
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/$BASEby name: the step 3 rebase made the two equal, but a later fetch can moveorigin/$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-assignBACKUPto 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=%sprintschore: WIP snapshot before rebase, then undo it withgit reset --soft HEAD~1so 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/$BASEleaves the tree identical, sogit diff "$BACKUP"stays empty while the branch's diff now reverts everything the base gained since the fork point. The block must end by printingFile-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 --porcelainoutput means tracked or untracked edits exist only in the working tree, made during commit recreation, and are not in$BACKUPor 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"- 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
- Analyze all changes (existing + new commits):
- Read the full diff:
git diff "origin/$BASE"...HEAD(re-assignBASEas in step 1) - Identify the type of change (feature, bugfix, refactor, etc.)
- Look for migration files, API spec changes, test coverage
- Read the full diff:
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 configand reading thetask_sourcerow (configuration lives in.claude/sova.db, not in a file).JIRA (
type = "jira"):- Do NOT use
Closes #N,Fixes #N, orResolves #N(those are GitHub Issue syntax) - Add a
## JIRAsection 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 invokedcontains a GitHub Issue number (e.g., #42), fetch the issue body (gh issue view 42 --json body) and check its## Acceptance Criteriasection against the actual diff on this branch:- If every criterion is met by this PR (or the issue has no
## Acceptance Criteriasection at all), add a## Issuesection containingCloses #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 themake checkandmake lintcriteria) 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## Issuesection containingPart of #42, a blank line,Not done in this PR:, and a bullet for each unmet, owner-only, or unknown criterion instead. ReserveCloses #42for 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
## Issueheading, not as trailing text inside another section, so a latersova-address-prskill round can find and replace it without disturbing other sections.
- If every criterion is met by this PR (or the issue has no
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_FILEto it.BODY_FILE=$(mktemp) cat > "$BODY_FILE" <<'EOF' [THE BODY] EOF echo "PR body saved to $BODY_FILE"- Do NOT use
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-skiponly 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; } fiIt 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, withBASEresolved 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 everyFAILline as blocking and everyWARNline as something to confirm before pushing.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 #12in prose closes #12 on merge. SetISSUEto the issue number fromthe 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 printedSTOPline (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. ASKIPline on stderr (exit 2) meanspython3cannot importsova(an active virtualenv hides the user-site install), so nothing was checked: run the same script with the interpretersovais installed under, or read the body file yourself for anyCloses,FixesorResolvesfollowed by an issue number other thanISSUE, 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- 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)
- New PR:
- 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 - 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 configtable, 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"
```
- 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).
- 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. Wait for CI pipeline: Wait for every check that gates the PR, not just one matched workflow run:
gh run listonly sees runs it triggered directly, so a check that arrives as a mirrored commit status (e.g. SonarCloud, which posts via.github/workflows/sonarcloud.ymlafter CI's coverage artifact lands) or from any other separate workflow can still be pending, or can later fail, after the one rungh run listmatched already passed.gh pr checksis 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 --requiredprintsno required checks reportedand exits 1 on every call.ghalso exits non-zero while a check is pending (8) or failing (1) and still prints the JSON then, so the block decides fromgh'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 --requiredlists 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--requiredreturns decides, even one the rulesets do not list (classic protection requires it too), so a failing classic-required check is never ignored. Only when--requiredreports 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 finalCI PENDINGverdict 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" fiMax 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
timeoutof 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 withCI PENDINGorNO 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-resolvesPR_NUMitself, since each call is a fresh shell.- 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 bucketpassorskippingand no required check is missing; continue to step 17 (askippingcheck, e.g. a path-filtered workflow, is treated as satisfied).CI FAILED: fetch logs for each check listed with a bucket other thanpass,skippingorcancel(gh run view <run_id> --log-failedwhenlinkpoints at a GitHub Actions run, otherwise openlink), 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 orgh run rerun) before continuing; do not proceed to step 17 with a canceled check.CI PENDINGonce 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 CHECKSonce 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): aghor network failure, not a CI result. Run the block again; if it fails the same way twice, stop and report the error.
- Self-review the PR diff:
Run the
sova-review-prskill against this PR to review the actual diff that will be merged:- Fetch the PR number from the branch
- Execute the
sova-review-prskill's analysis in full (fetch diff, read files, deep analysis) - Post the review on GitHub
- If the verdict has no findings >= 3/10: skip step 18, go to step 19
- If there are findings >= 3/10: continue to step 18
- Address review findings:
Run the
sova-address-prskill to fix the findings:- Score and address each finding (fix or acknowledge)
- Commit fixes
- Reply to review comments on GitHub
- Resolve threads
- Re-run step 9's checks, then force push:
git push --force-with-lease - Wait for CI again (go back to step 15 logic, max 3 total CI cycles across the entire run)
- 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.
- 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-reviewskill or thesova-review-fullskill to catch issues before pushing - Full workflow: the
sova-develop-fullskill -> thesova-review-fullskill -> thesova-prskill -> thesova-integrate-prskill - After merge: Run the
sova-integrate-prskill for merge, cleanup, and knowledge extraction - Need to reorganize commits first? Run the
sova-rearrange-commitsskill
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-leasefor 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
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
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.

