Create a PR
Write the title and the description, then run the linter before you post:
python3 .agents/skills/create-pr/lint_pr.py lint \
--title "fix(libero): reset action queues between episodes" \
--body-file /tmp/pr-body.md
Exit code 1 means at least one ERROR — fix it and re-run. It is the same checker you point at a PR that is already open:
python3 .agents/skills/create-pr/lint_pr.py lint --pr 1444
The rules come from CONTRIBUTING.md ("PR Title and
Description"), .github/PULL_REQUEST_TEMPLATE.md,
and — for the title — the required pr-title-check job, which runs
openGemini/pr-title-checker
with strict: true. Every title rule that job enforces is an ERROR in the
linter, so a clean lint means the check passes. Paths below are relative to
the repo root.
Why this exists
Over the 25 most recently merged PRs the linter reports errors on 11 of
them. The dominant failures, in order: template sections dropped (14),
Types of changes / Checklist with nothing ticked (9), template
<!--- ... --> hints left in (9), prose above the first heading (5). PR
#1421 is the worst case — the whole
body was pasted with a two-space indent, which nests every ### heading inside
the Description and breaks the results table outright.
1. Gather the facts before writing a word
Never describe the PR from memory of what you intended. Read the diff:
git fetch origin main
git log --oneline origin/main..HEAD
git diff --stat origin/main...HEAD
You need, concretely:
- Which component the diff touches — that becomes the scope.
- Every user-visible change — a new config key, a renamed flag, a moved doc page, a changed default. Internal churn does not go in the description.
- What you actually ran to verify it — the e2e config name, the eval
numbers,
pre-commit run --all-files. "Tested locally" is not an answer. - The issue it closes, if any.
2. Title
<type>(<scope>): <description>
- type:
featfixdocsstylerefactortestchoreperfbuildcirevert. Lowercase. Onmainthe distribution is roughlyfeat166 /fix122 /docs76 /chore18 /refactor14. - scope: the component the diff touches —
embodiment,libero,realworld,readme,docker,collective,openpi_pytorch. Optional; drop it rather than invent one.[a-z0-9_-]only — a space, slash, dot or capital fails CI.feat(new)(PR #1421) passes CI but is the anti-pattern the linter warns on: it names the PR, not the code. - description: ≤ 50 characters, and that is the part after
:, not the whole title. This is the rule PRs fail most often. It must start with a lowercasea-z(sosupport 5D parallelism, never5D parallelism support), stay imperative, carry no trailing period, and use plain ASCII —π₀in a title fails the check.add MolmoAct2 LIBERO evaluation support, notAdded support for MolmoAct2. - Never append
(#123)— GitHub adds it at squash-merge.
Fitting 50 characters is the whole discipline of the title: name the one change
a reviewer needs to see in the PR list, and let the body carry the rest. If the
PR does several things, title it after the largest and enumerate in
Description.
Check just the title, no body needed:
python3 .agents/skills/create-pr/lint_pr.py lint --title "feat(new): add stuff."
3. Body
Start from the skeleton so no section goes missing:
python3 .agents/skills/create-pr/lint_pr.py template > /tmp/pr-body.md
Then fill it in. Keep all six headings, even where a section is one line. What each one is for:
| Section | What goes in it |
|---|---|
Description |
One sentence on what changed and where, then a bullet per user-visible change. ≤ 250 words, ≤ 12 bullets. |
Motivation and Context |
Why the change is needed. Link the issue (Fixes #NNN). |
How has this been tested? |
The command you ran and its outcome. Required by CONTRIBUTING.md when the PR can move the reward curve — give the numbers. |
Additional information |
Figures, logs, repro commands. May be empty. |
Types of changes |
Tick what applies. One or two boxes, not four. |
Checklist: |
Tick what applies. CONTRIBUTING.md: a PR with an empty Description or Checklist is marked draft and not reviewed. |
Style rules the linter enforces:
- No leading indentation. Not on headings, not on paragraphs, not on table rows. Four spaces turns a paragraph into a code block; a table whose header and rows disagree on indent does not render as a table at all.
- Delete the
<!--- ... -->hints once a section is written. - Nothing above the first
###heading — put it inDescription. - One line per paragraph. GitHub reflows; hard wraps only survive until someone edits the body.
4. Lint, then create
python3 .agents/skills/create-pr/lint_pr.py lint \
--title "$TITLE" --body-file /tmp/pr-body.md
Add --docs-only for a documentation-only PR to drop the "how was this tested"
warning. Add --quiet to omit the suggested fixes. Once it exits 0:
gh pr create --base main --title "$TITLE" --body-file /tmp/pr-body.md
Commits must be signed off (git commit -s) and follow the same
<type>(<scope>): <description> format — DCO checks the trailer, so make sure
your name is not wrapped in quotes.
5. Fixing a PR that is already open
python3 .agents/skills/create-pr/lint_pr.py lint --pr 1421
For the indent/wrap damage specifically, fix rewrites the body: it strips the
paste offset (preserving genuine list nesting), pulls headings and tables flush
left, and drops the template hints. --unwrap additionally joins hard-wrapped
prose into one line per paragraph.
python3 .agents/skills/create-pr/lint_pr.py fix --pr 1421 --unwrap > /tmp/fixed.md
python3 .agents/skills/create-pr/lint_pr.py lint --title "$TITLE" --body-file /tmp/fixed.md
gh pr edit 1421 --body-file /tmp/fixed.md
gh pr edit 1421 --title "feat(embodiment): add MolmoAct2 LIBERO evaluation support"
fix only de-indents a body the linter already flagged, so running it on a
well-formed PR is a no-op apart from comment removal. Read the output before
posting it — it is a mechanical rewrite, not an editor.
Gotchas
lint --pr Nshells out togh pr view, so it inherits yourghauth and the repo you are standing in. It is read-only; nothing is posted until you rungh pr edit/gh pr createyourself.- The skeleton from
templatefails its own lint — deliberately. It ships with no boxes ticked, andcheckbox-untickedis an ERROR. Tick them. - The linter accepts
##as well as###for section headings, because GitHub renders both. What it will not accept is a renamed section: a body using## Testinginstead of### How has this been tested?reportssection-missing. - Warnings do not fail the run (exit 0).
body-hardwrap,body-commentsandtitle-scope-vagueare judgement calls — but a reviewer reads them the same way the linter does. fixcannot recover a broken code fence. Ifbody-fencefires (odd number of```), everything after the stray fence has been swallowed by GitHub's renderer; find it and close it by hand.- Tables inside
<details>still need flush-left rows. The<details>wrapper does not exempt them. - A good commit subject is not always a legal PR title.
commit-checkandpr-title-checkare different jobs with different limits — commit subjects get ~72 characters, PR descriptions get 50. A one-commit PR that reuses its subject verbatim can fail.
Troubleshooting
error: gh pr view N failed: ... could not resolve to a PullRequest— wrong repo.cdto the RLinf checkout, or pass the number of a PR in this repo.[E] body/body-indentedpersists afterfix— the body mixes two paste offsets (e.g. part typed, part pasted). Dedent by hand;fixonly removes a single uniform offset.[W] body/section-order— you kept all the headings but reordered them. Harmless, but reviewers scan for the template order.lint --titlealone reports0 error(s)on an empty body — that is correct, it only checks what you gave it. Pass--body-filetoo before you create.