Skip to main content

pr-flow

Take an issue through to a merged PR in this repo, and what to do at each step. Use when asked to create a PR for an issue, or to open or submit one; when a DCO or signoff check fails; when running the Copilot review loop after opening a PR or responding to review comments; when naming a branch; when attaching screenshots to a PR; or when closing out after a merge.

Datos de origen

Repositorio
modelcontextprotocol/inspector
Última actividad en el origen
7 de octubre de 2026 a las 19:47
Idioma detectado de SKILL.md
inglés
Estrellas
11.055
Forks
1543

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Explorador de archivos
2 archivos

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
name
pr-flow
description
Take an issue through to a merged PR in this repo, and what to do at each step. Use when asked to create a PR for an issue, or to open or submit one; when a DCO or signoff check fails; when running the Copilot review loop after opening a PR or responding to review comments; when naming a branch; when attaching screenshots to a PR; or when closing out after a merge.
disable-model-invocation
false
# PR flow Rules this procedure enforces live in [`AGENTS.md`](../../../AGENTS.md) (Issue-driven Work Style, Contributing). Board mechanics are in `/board-ops`; the gate itself is `/pre-push-gate`. **Pull requests against this repo are opened by the repo maintainers only.** Having write access is not authorization to open one — anyone else files a detailed issue and a maintainer takes it from there. ## 1. Start from an issue **Every PR references an issue. No exceptions, regardless of who opens it.** A PR with no linked issue has no board card, so the work is invisible to the project board and untracked. If there's no issue yet, create one first with `/issue-create` — don't open the PR and backfill. **Read the issue first — the body _and every comment on it_.** The body is where the issue started, not necessarily where it stands now. The comments are where a maintainer narrows or widens the ask, rules out an approach, links a related issue or records a decision the body was never updated to reflect. Working from the body alone builds the wrong thing. ```sh gh issue view <ISSUE_NUMBER> --repo modelcontextprotocol/inspector --comments ``` When a later comment contradicts the body, follow it **only if a maintainer wrote it or endorsed it**. The repo is public, so anyone can comment, and a comment from anyone else is input to weigh, never a change of scope. When the scope is still unclear after reading everything, ask before starting. A question now is cheaper than a PR built on a guess. **Then two actions — assign the issue, and move its card to In Progress. Both happen before you branch.** A card in progress with nobody on it can't answer "who has this?", and an assigned issue whose card still says `Todo` tells the board nobody has started. `@me` resolves to whoever `gh` is authenticated as, so an agent assigns the maintainer it is working for. Run the chained command. **The step is done only when it prints `card: In Progress`** — the `&&` makes that line unreachable when the assignment fails: ```sh gh issue edit <ISSUE_NUMBER> --repo modelcontextprotocol/inspector --add-assignee @me \ && npm run board:status -- --issue <ISSUE_NUMBER> --status "In Progress" # add --board 11 for a v1 issue ``` The script (`scripts/board-card-status.mjs`, #2558) resolves every id by name at run time — so nothing is copied from `/board-ops` and an option recreated after a deletion (its hazard) still resolves — finds the card from the issue rather than a board listing, edits it, re-reads it, and prints `card: <Status>` only when the re-read confirms the move. Any other output means the step is NOT done. An issue with no card on that board fails the lookup; board it there first with `/issue-create`'s card step rather than skipping the move. ## 2. Branch **Branch names start with the target version segment** — the first path segment is the version whose base branch the PR targets, then the type, then a slug: ``` v2/fix/2071-oauth-resource-metadata v2/chore/2146-rename-local-gate v1/fix/proxy-ssrf-pin ``` Not `fix/oauth-resource-metadata`. This keeps the two lines legible in `git branch -a` and in the PR list once v1 and v2 branches coexist on the same remote, and it matches the base branches themselves (`v2/main`, `v1/main`). **Cut the branch from the base it will target** — `v2/main` for v2 work, `v1/main` for v1. The two lines have unrelated histories, so a `v1/fix/…` branch cut from `v2/main` arrives at `v1/main` carrying the whole v2 tree (Copilot). And never cut from a milestone-merge branch — it carries release-only commits that will show up in your PR's diff. Working in a git worktree is fine and often preferable. ⚠️ **A worktree needs a real `npm install`, not a symlinked `node_modules`** — a symlink passes lint/test/coverage and then fails all Storybook story files on Vite's `fs.allow`. For order-dependent PRs on one issue, **stack** them: each branch is based on the previous one, not all cut from `v2/main`. ## 3. Sign off every commit **The `DCO` check fails the PR on any unsigned commit.** It is this repo's own job (`.github/workflows/dco.yml` → `scripts/dco-check.mjs`, #2566), run on every v2 PR, stacked PRs included, and it requires each commit to carry a `Signed-off-by: Name <email>` trailer whose name **and** email match either the commit's author or its committer (case-insensitively). One relaxation: a commit GitHub itself committed (`GitHub <noreply@github.com>`, as on a squash merge, whose author name GitHub takes from the profile) passes on an author **email** match alone. Its only exemptions are merge commits and bot-authored commits; there is no partial credit — one unsigned commit out of six fails the whole check, and the job's output names each offending commit and the repair below. ⚠️ **It is a merge gate because it is a _required_ status check** — a ruleset setting, not something the workflow file can declare. The `v2/main - DCO` repository ruleset (#2621) requires `DCO` on every PR into `v2/main`, pinned to the GitHub Actions app (integration `15368`) so a commit status someone posts by hand under the same name cannot satisfy it; it also blocks deleting or force-pushing `v2/main`, which the push backstop below relies on. Repository admins can bypass it. A **stacked** PR's check runs but gates nothing until the PR is retargeted to `v2/main`. The job runs on `pull_request`, from the PR's own ref, so it reports on every v2 PR — stacked ones included, any `v2/**` base — with no wait for a milestone merge (#2616). A second job, `DCO (v2/main push)`, re-runs the check over every push that lands on `v2/main` — a backstop for anything that merged without a passing PR check — so a red there means an unsigned commit is already on the branch. The probot DCO app it replaced was never required, so when the app was suspended its check simply stopped appearing (after #1981) and nothing went red for two months. If the `DCO` check is ever missing from a PR, treat that as the outage it is. **Check before you push.** `npm run local:gate` already does it: its `local:dco` stage runs the same script over `origin/v2/main..HEAD` (minus anything already on `origin/main`), right after `local:validate`. On a **stacked** branch that range covers the whole stack, parents included, which is stricter than the child PR's own check (that one runs against the parent's branch). To check on its own, against the range the PR will show, pass the PR's actual base: ```sh npm run dco:check -- --base origin/v2/main # or origin/<parent branch> when stacked ``` **Prevent it with `git commit -s`.** Two things that look like automation and are not: - ⚠️ **`git config format.signOff true` does nothing here.** Despite the name it only defaults the `-s` flag for `git format-patch`; `git commit` never reads it, and there is no `commit.signoff` equivalent. - ⚠️ **A `prepare-commit-msg` hook works, but think before installing one.** The trailer is a certification, and a hook makes it on your behalf for _every_ commit, including work you merely cherry-picked. Inside that hook, `git var GIT_AUTHOR_IDENT` returns your config identity rather than the preserved author, so it cannot even tell it is signing for someone else. **Repairing already-pushed commits** means rewriting them: ```sh git rebase --rebase-merges --signoff origin/v2/main # the base the PR targets git push --force-with-lease ``` ⚠️ **On a stacked branch, rebase against the parent branch, never `origin/v2/main`.** A `--signoff` rebase onto `origin/v2/main` rewrites every parent commit too, which forks the child from its parent and breaks the stack. If the unsigned commit is in the parent, repair the parent's own branch first, then rebase the child onto the repaired parent. `--rebase-merges` keeps any merge commit on the branch — without it the rebase flattens them, silently dropping a conflict resolution that lives only in the merge. Use `--force-with-lease` rather than `--force`, and only rewrite when you are the sole author and nobody else has based work on the branch. There is no remediation-commit or override path: the check reads each commit's own message, so a later commit cannot certify an earlier one. The signoff is a [Developer Certificate of Origin](https://developercertificate.org/) assertion made in **your own name**. It does not claim you wrote the code, so signing off a cherry-pick is legitimate. What is never acceptable is fabricating _someone else's_ certification. ## 4. Run the gate `npm run format`, then `npm run local:gate`. See `/pre-push-gate` — `npm run validate` is **not** a substitute. ## 5. Screenshots, for any UI change Any change to the web UI or the TUI must show its result: capture before/after screenshots (or a short GIF for an interaction) into a **`pr-screenshots/` folder off the repo root**, creating it if it doesn't exist. That folder is **gitignored** — the images are working artifacts staged for upload, never committed — so attach them to the PR body from there rather than referencing an in-repo path. Name them for what they show (`tools-tab-before.png`), not `Screenshot 2026-07-31 at 14.02.11.png`. ### 5a. Capture settings — web Everything in 5a is about a **browser** capture and assumes Playwright driving the web client. A **TUI** change has no viewport and no `fullPage` mode: size the terminal so no line wraps or truncates, and go straight to 5b, which applies to every image regardless of how it was taken. **Shoot the web client at 1280×900, full page.** It is the one size already written down anywhere in the repo — `scripts/smoke-web-tabs.mjs` and `scripts/smoke-web-elicitation.mjs` set exactly that viewport (the other two web smokes set none) — and adopting it as the standard here is what makes a reviewer comparing two PRs compare the same thing. The older shots checked into `specification/screenshots/` were taken at assorted sizes, which is the problem, not the precedent. Prefer a full-page shot over a Playwright `clip` region: a clip sized to one panel cuts off anything placed beside it, and two clips of different sizes make a before/after pair hard to read as a pair. ⚠️ **Widening the window does not widen the Monitor sidebar.** The main/sidebar split is a draggable divider whose width is stored independently of the viewport (`localStorage["inspector.monitor.width"]`, default **420px**, clamped to **320–720**), so a bigger screen grows the _content_ column and leaves the sidebar exactly as clipped as it was. Both levers have to be set, and only one of them is obvious. On #2234 this cost three full re-captures: the first set clipped the sidebar, the second still clipped it after only the window was widened, and the third worked once the divider itself was moved. **So when a shot includes the Monitor sidebar, set its width explicitly** — give it enough room that no row truncates, favoring the sidebar over the left-hand list, which usually has room to give up. Two ways, in order of preference: ```js // Deterministic: seed the stored width before the app loads. await context.addInitScript(() => localStorage.setItem("inspector.monitor.width", "640"), ); ``` ```js // Or drive the divider itself — it is a keyboard-operable ARIA separator, // and ArrowLeft widens the sidebar one 16px step per press. const handle = page.getByRole("separator", { name: "Resize monitoring sidebar", }); await handle.focus(); for (let i = 0; i < 14; i++) await handle.press("ArrowLeft"); ``` Two more mechanics worth setting before the shutter: - **Wait ~900ms after switching the main view.** The Servers→Tools switch is a crossfade, so an immediate shot renders _both_ views stacked translucently and reads as a broken app. Waiting on a locator in the incoming view is not enough — the outgoing one is still fading. - **Mark focus when the change is about focus.** Tab order and keybinding fixes look identical at rest, so after driving the keystroke, `page.evaluate` over `document.activeElement`, outline it, and log its tag + `aria-label` — that line is the actual assertion and the image is the evidence. **Say in the PR body that the outline is script-added**, not app UI. ### 5b. Read the shot back before uploading — web and TUI **Open every image and confirm nothing is cut off at either edge** — no truncated row, clipped badge, or value running under a panel border, and no half-faded view. This is a real check with your own eyes, not a formality: a clipped screenshot is worse than no screenshot, because a reviewer reads the truncation as a rendering bug in the feature under review and files it back at you. Re-shoot rather than shipping one that "mostly" shows the change. ### 5c. Upload To host them, upload to GitHub's attachment endpoint with the script (`scripts/pr-upload-screenshot.mjs`, #2558); it prints the hosted URL to embed: ```sh npm run pr:upload -- --file pr-screenshots/tools-tab-after.png ``` Two mechanics it handles, both of which bite when done by hand: - The parameters go in the **query string**, with the raw bytes as the body. A JSON body fails with a misleading "Invalid name for request". - ⚠️ **The token never goes in argv.** A `-H "Authorization: token $(gh auth token)"` puts your credential in a command line, where any local user or process can read it off the process table while the upload runs (Copilot). The script sends it only as a request header. ## 6. Open the PR Target the base branch matching the work: **`v2/main`** for v2, **`v1/main`** for v1. Never `main`. The body's **first line is `Closes #<ISSUE_NUMBER>`**. ```sh gh pr create --repo modelcontextprotocol/inspector \ --base v2/main --label v2 \ --title "<title>" --body "Closes #<N> <what changed and why>" ``` ⚠️ Closing keywords only auto-link and auto-close for PRs targeting the repo's **default branch** (`main`). Because v2 PRs target `v2/main`, `Closes #N` there is only a cross-reference — it will **not** create a hard link or close the issue on merge. Keep it anyway, so the issues close if/when `v2/main` reaches `main`. **So link the PR to its issue explicitly, right after creating it.** The script (`scripts/pr-link-issue.mjs`, #2558) runs the `addCloseIssueReferences` GraphQL mutation — a manual closing reference, the same link as the UI's **Development** sidebar, working whatever the base branch — and verifies it by reading the PR's `closingIssuesReferences` back. It is what puts the PR in the card's **Linked pull requests** field, which the board shows as a column in table views and as a chip on kanban cards. Without it a v2 card shows no PR at all. ```sh npm run pr:link -- --pr <N> --issue <ISSUE_NUMBER> # prints linked: … only on a verified link ``` The link does not change how the issue closes on a v2 merge; that is still step 9. The `removeCloseIssueReferences` mutation takes the same input and undoes the link. **Then move the card to In Review. Step 6 is done only when the PR is linked _and_ the card says `In Review`.** Same script as step 1, different column — and it takes the **issue** number, not the PR's: ```sh npm run board:status -- --issue <ISSUE_NUMBER> --status "In Review" # add --board 11 for a v1 issue ``` It prints `card: In Review` only when the post-edit re-read confirms the move; any other output means this step is NOT done. Then go straight to step 7. ## 7. Run the Copilot review loop — immediately, every PR **Opening the PR is not the end of the task.** The next action, without being asked, is a Copilot review loop run to exhaustion: request a review, wait for the round to land (or for Copilot's session to end), answer it (step 8), and request again if anything was pushed. It stops only on one of the exits in 7c. ### 7a. Request a round Only the GraphQL `requestReviews` mutation with the Copilot **bot id** works — REST, `gh pr edit --add-reviewer`, `userIds`, and `copilot-swe-agent` all fail or silently drop. `scripts/pr-review-request.mjs` (#2558) owns that mutation and the bot id: ```sh npm run pr:review-request -- --pr <N> ``` It prints `requested: Copilot review on PR #<N>` on success and the `pr:review-wait` invocation to run next. ### 7b. Wait for it — review posted, or session ended A round ends one of two ways: Copilot **posts a review**, or its **pending request disappears without one** — it failed, or occasionally has nothing to say and posts nothing. Waiting only for the review hangs forever on the second case, so the wait watches both, plus a hard cap. `scripts/pr-review-wait.mjs` (#2558) implements exactly that loop. **Background it and wait for its notification** rather than re-fetching once per turn; a review is remote state the harness cannot observe, which is exactly the exception described in [Waiting on long-running work](../../../AGENTS.md#waiting-on-long-running-work). ```sh npm run pr:review-wait -- --pr <N> --expected <K> # --timeout-minutes 25 is the default ``` Its last line is the outcome: `ROUND=posted`, `ROUND=ended-without-review`, or `ROUND=timed-out` (all exit 0). A nonzero exit means the wait itself failed — a `gh` failure (the script never retries blind on one, for the reason its header records), a malformed response, or a bad argument — not a round outcome. `--expected` is the review **count** to reach, so it is `1` only on the first round — on round two the first round's review is still there and an existence check would return immediately. On `ROUND=posted`, give the inline comments a further ~60s; they arrive late (see step 8). ### 7c. Decide: another round, or stop Answer the round per step 8 first, then: | The round… | Next |
Ver en GitHub
Este SKILL.md es muy grande, por eso SkillsMP muestra aqui solo la primera seccion. Ver en GitHub