Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
187 commits
Select commit Hold shift + click to select a range
cd4ccf9
fix(auth): stop stale-snapshot writers from clobbering oauth.json
BobDickinson Sep 24, 2026
1f957ff
docs(pr-flow): make an exhaustive Copilot review loop the step after …
cliffhall Sep 24, 2026
3be44dc
docs(pr-flow): stop on a timed-out round instead of re-requesting (#2…
cliffhall Sep 24, 2026
2623167
docs(pr-flow): link a v2 PR to its issue with addCloseIssueReferences…
cliffhall Sep 24, 2026
d483427
Merge pull request #2466 from modelcontextprotocol/v2/docs/2464-copil…
cliffhall Sep 24, 2026
8549ef0
feat(auth): move OAuth tokens and client secrets into the secret store
BobDickinson Sep 24, 2026
4b99ef3
docs(pr-flow): assign the issue to yourself when work starts (#2467)
cliffhall Sep 24, 2026
defeb29
Merge pull request #2468 from modelcontextprotocol/v2/docs/2467-pr-fl…
cliffhall Sep 24, 2026
e25abc9
fix(skills): require ttlMs and cacheScope on modern skills/get result…
cliffhall Sep 24, 2026
7169168
fix(core): advertise MCP Apps UI extension only from a client that re…
cliffhall Sep 24, 2026
4f32dd1
fix(cli): end the long-lived stream cleanly on EPIPE (#2412)
cliffhall Sep 24, 2026
ec57950
fix(web): mask the tail of a form secret split by a raw & (#2422)
cliffhall Sep 24, 2026
014621a
Merge pull request #2469 from modelcontextprotocol/v2/fix/2404-skills…
cliffhall Sep 24, 2026
ce086f4
docs(pr-flow): make the In Progress / In Review card moves runnable, …
cliffhall Sep 24, 2026
e448d14
fix(skills): report a "dynamic" skill as unverifiable, not verified (…
cliffhall Sep 24, 2026
eab31f7
docs(skills): anySkillUnverifiable reads the selected outcome, not th…
cliffhall Sep 24, 2026
a183722
docs(skills): lead testing's description with writing coverage (#2459)
cliffhall Sep 24, 2026
d60a7d1
fix(web): claim MCP Apps rendering only when the sandbox is available…
cliffhall Sep 24, 2026
6e7dae0
Merge branch 'v2/main' into v2/fix/2405-dynamic-skill-unverifiable
cliffhall Sep 24, 2026
c8bce7f
fix(gate-lease): serve queued local:gate runs in arrival order (#2473)
cliffhall Sep 24, 2026
3436c93
Merge pull request #2474 from modelcontextprotocol/v2/fix/2405-dynami…
cliffhall Sep 24, 2026
fb6115b
Merge branch 'v2/main' into v2/fix/2422-form-mask-unescaped-amp
cliffhall Sep 24, 2026
4c1fa3d
docs(test-servers): constructor takes the RESOLVED ServerConfig, not …
cliffhall Sep 24, 2026
f883eb3
fix(gate-lease): name the queue, not a holder, when the lock is free …
cliffhall Sep 24, 2026
80cda40
Merge pull request #2475 from modelcontextprotocol/v2/fix/2422-form-m…
cliffhall Sep 24, 2026
e4c8ab0
fix(remote): re-arm relay waits for string progress tokens (#2458)
cliffhall Sep 24, 2026
538c568
Merge branch 'v2/main' into v2/fix/2473-gate-lease-fifo
cliffhall Sep 24, 2026
2ebadf4
fix: renderer-aware Server Settings toggle; reject inert --advertise-…
cliffhall Sep 24, 2026
09ca92a
Merge pull request #2476 from modelcontextprotocol/v2/fix/2473-gate-l…
cliffhall Sep 24, 2026
deb1a71
Merge branch 'v2/main' into v2/fix/2403-apps-extension-opt-in
cliffhall Sep 24, 2026
1c944b7
Merge branch 'v2/main' into v2/fix/2458-string-progress-token
cliffhall Sep 24, 2026
7d6ce32
Merge branch 'v2/main' into v2/fix/2412-cli-stream-epipe
cliffhall Sep 24, 2026
e4a3df5
docs: reflect that acquired OAuth tokens now live in the secret store
BobDickinson Sep 24, 2026
fd6a584
fix(tui): cap rendered request/response body lines (#2407)
cliffhall Sep 24, 2026
2806b4f
Merge pull request #2471 from modelcontextprotocol/v2/fix/2403-apps-e…
cliffhall Sep 24, 2026
af04a53
Merge branch 'v2/main' into v2/chore/2459-copilot-test-servers-handoff
cliffhall Sep 24, 2026
cfdbe57
docs(pr-flow): fail step 1's check on a failed assignment; select the…
cliffhall Sep 24, 2026
4853115
Merge pull request #2478 from modelcontextprotocol/v2/fix/2458-string…
cliffhall Sep 24, 2026
eec0349
Merge pull request #2479 from modelcontextprotocol/v2/fix/2412-cli-st…
cliffhall Sep 24, 2026
6dbdc8f
Merge branch 'v2/main' into v2/fix/2407-tui-body-line-cap
cliffhall Sep 24, 2026
412944d
docs(pr-flow): name the selected board in the no-card fallback (#2472…
cliffhall Sep 24, 2026
0582437
Merge pull request #2477 from modelcontextprotocol/v2/chore/2459-copi…
cliffhall Sep 24, 2026
b436906
Merge pull request #2480 from modelcontextprotocol/v2/fix/2407-tui-bo…
cliffhall Sep 24, 2026
06cd7e0
Merge pull request #2472 from modelcontextprotocol/v2/docs/2470-pr-fl…
cliffhall Sep 24, 2026
64007e7
fix(cli): redact URL query secrets in the error envelope (#2423)
cliffhall Sep 24, 2026
6761418
chore(smoke): run smoke:tui for real in GitHub CI (#2408)
cliffhall Sep 24, 2026
d5de6ee
fix(cli): match URL schemes case-insensitively when redacting (#2488 …
cliffhall Sep 24, 2026
65a31f4
fix(auth): address review — colon-free store ids, non-durable preserv…
BobDickinson Sep 24, 2026
7592d28
docs(pr-flow): read the issue and all its comments before starting wo…
cliffhall Sep 24, 2026
d780fd6
docs(pr-flow): only maintainer comments change an issue's scope (#249…
cliffhall Sep 24, 2026
82adeee
fix(cli): split comma-joined URLs and redact through apostrophes (#24…
cliffhall Sep 24, 2026
10bc31a
fix(auth): address review round 2 — confirmed deletes, policy-aware c…
BobDickinson Sep 24, 2026
e00aaed
Merge pull request #2491 from modelcontextprotocol/v2/docs/2487-pr-fl…
cliffhall Sep 24, 2026
c5d2a34
Merge pull request #2488 from modelcontextprotocol/v2/fix/2423-cli-er…
cliffhall Sep 24, 2026
56ba766
Merge pull request #2489 from modelcontextprotocol/v2/chore/2408-tui-…
cliffhall Sep 24, 2026
5e0cc0f
chore(deps): upgrade the MCP TypeScript SDK to 2.1.0
cliffhall Sep 24, 2026
17dc258
test(core): pin custom Authorization vs OAuth token precedence (#2492…
cliffhall Sep 24, 2026
3ab5c8d
test(core): cover SSE in the Authorization precedence test (#2492 rev…
cliffhall Sep 24, 2026
6a31cce
Merge pull request #2492 from modelcontextprotocol/v2/chore/2486-sdk-…
cliffhall Sep 24, 2026
01e9374
fix(web): tell the user to reconnect after editing custom headers whi…
cliffhall Sep 24, 2026
e60a7c0
test(web): complete App.test's fake client and cover the reconnect no…
cliffhall Sep 24, 2026
abe304f
test(web): declare disconnect on the roots fake client type (#2460)
cliffhall Sep 24, 2026
89d7cf5
fix(web): defer the reconnect notice to the settled write, and compar…
cliffhall Sep 24, 2026
ecaf8c3
fix(web): raise the reconnect notice only for the still-active server…
cliffhall Sep 24, 2026
844c81a
fix(core): snapshot the settings the transport factory was handed (#2…
cliffhall Sep 24, 2026
2f8d6b9
test(web): cover withdrawing the reconnect notice when the connection…
cliffhall Sep 24, 2026
682bd6b
Merge remote-tracking branch 'origin/v2/main' into v2/fix/2460-custom…
cliffhall Sep 24, 2026
8bdf395
Merge pull request #2493 from modelcontextprotocol/v2/fix/2460-custom…
cliffhall Sep 24, 2026
a837158
fix(auth): address review round 3 — file-store confirmed deletes, loc…
BobDickinson Sep 24, 2026
a7cea03
fix(auth): roll back unindexed store secrets when the residue write f…
BobDickinson Sep 24, 2026
961cb16
fix(auth): restore prior store secrets when the OAuth residue write f…
BobDickinson Sep 24, 2026
6aba8d8
fix(auth): enforce file/store consistency invariant across every comb…
BobDickinson Sep 24, 2026
64e27b8
fix(auth): make deleteClientConfigStore all-or-nothing
BobDickinson Sep 24, 2026
d6bee93
fix(auth): settle parallel store mutations before rollback; transacti…
BobDickinson Sep 24, 2026
6f70aec
fix(auth): compensate partial keychain purges; move sections descript…
BobDickinson Sep 25, 2026
39d548c
fix(auth): compensate a partially-committed keychain delete in delete…
BobDickinson Sep 25, 2026
5178060
fix(auth): lock OAuth reads against torn residue/secret joins; keep l…
BobDickinson Sep 25, 2026
a2ba182
fix(auth): handle __proto__ keys in persistence maps (prototype-pollu…
BobDickinson Sep 25, 2026
92b6620
fix(auth): round-13 polish — accurate migration warning, strict write…
BobDickinson Sep 25, 2026
f58b3d8
fix(auth): use own-property reads for untrusted map keys and reject p…
BobDickinson Sep 25, 2026
d883fad
fix(auth): validate store values in migration, blob map shapes, and t…
BobDickinson Sep 25, 2026
5281558
chore(scripts): fail fast when node_modules is older than its lockfil…
cliffhall Sep 25, 2026
641765a
Merge pull request #2495 from modelcontextprotocol/v2/chore/2494-veri…
cliffhall Sep 25, 2026
175eca2
fix(auth): strict OAuth hydration, schema-matched stored-value valida…
BobDickinson Sep 25, 2026
d27a6a2
fix(smoke): parse CLI error envelope from last stderr line
BobDickinson Sep 25, 2026
0785023
fix: refuse OAuth state mutations when oauth.json is unrecognized
BobDickinson Sep 25, 2026
bc98594
docs: add MCP Inspector: Our AI Software Factory
BobDickinson Sep 25, 2026
0a942dd
docs: address Copilot review on #2498
BobDickinson Sep 25, 2026
869de23
fix: enforce a body-size cap on POST /api/storage/:storeId
BobDickinson Sep 25, 2026
581e387
fix: split registration_access_token into the secret store
BobDickinson Sep 26, 2026
6ebb203
fix(cli): surface secret-store failures instead of reporting no_store…
BobDickinson Sep 26, 2026
bf7b395
fix(cli): bound the OAuth browser open and say when it fails (#2410)
cliffhall Sep 26, 2026
aeba2ec
chore(tui): justify every exhaustive-deps suppression (#2414)
cliffhall Sep 26, 2026
3094905
test(launcher,cli): assert Windows backslash paths survive argv forwa…
cliffhall Sep 26, 2026
483348c
fix(scripts): fail the Dependabot sweep clearly on a bad alert listin…
cliffhall Sep 26, 2026
40184da
fix(docker): derive the HEALTHCHECK probe address from HOST (#2424)
cliffhall Sep 26, 2026
f3e7c66
docs(cli): state the real purpose of the openUrl timeout (#2499 review)
cliffhall Sep 26, 2026
501ec1c
chore(smoke): surface the app's connect error on a connect timeout (#…
cliffhall Sep 26, 2026
2d3e8da
fix(scripts): reject a zero-page alert listing (#2502 review)
cliffhall Sep 26, 2026
3186bc4
fix(core): enforce one deadline per RequestInit in withOAuthRequestTi…
cliffhall Sep 26, 2026
cba3f72
Merge pull request #2499 from modelcontextprotocol/v2/fix/2410-cli-op…
cliffhall Sep 26, 2026
428bc04
Merge branch 'v2/main' into v2/chore/2414-tui-exhaustive-deps-justify
cliffhall Sep 26, 2026
c7f46c0
Merge branch 'v2/main' into v2/chore/2418-request-deadlines-invariant
cliffhall Sep 26, 2026
61abd95
Merge branch 'v2/main' into v2/chore/2416-windows-path-argv
cliffhall Sep 26, 2026
757ad4e
Merge branch 'v2/main' into v2/fix/2424-healthcheck-host
cliffhall Sep 26, 2026
8841ea9
fix(docker): trim CLIENT_PORT in the healthcheck probe as the server …
cliffhall Sep 26, 2026
8e92f7f
Merge remote-tracking branch 'origin/v2/fix/2424-healthcheck-host' in…
cliffhall Sep 26, 2026
b43a5eb
fix(server): drive rename secret copy from strict reads; name the rig…
BobDickinson Sep 26, 2026
73bdb79
Merge pull request #2500 from modelcontextprotocol/v2/chore/2414-tui-…
cliffhall Sep 26, 2026
bcaa42c
Merge pull request #2501 from modelcontextprotocol/v2/chore/2416-wind…
cliffhall Sep 26, 2026
d1f1688
Merge pull request #2503 from modelcontextprotocol/v2/chore/2418-requ…
cliffhall Sep 26, 2026
b26b8fa
Merge pull request #2504 from modelcontextprotocol/v2/fix/2424-health…
cliffhall Sep 26, 2026
92c053f
Merge pull request #2505 from modelcontextprotocol/v2/chore/2496-smok…
cliffhall Sep 26, 2026
18f5623
fix(auth): pin the AES-GCM auth tag length in FileSecretStore
cliffhall Sep 26, 2026
99ba6eb
ci: split the npm release job so install scripts never hold the OIDC …
cliffhall Sep 26, 2026
b50ce03
ci: update the workflow header for the package/publish split
cliffhall Sep 26, 2026
9427cea
Merge pull request #2502 from modelcontextprotocol/v2/fix/2425-depend…
cliffhall Sep 26, 2026
21090ba
test(auth): assert the GCM tag-length pin at cipher construction
cliffhall Sep 26, 2026
b6278fd
ci: refuse a tarball carrying publishConfig and pin the publish registry
cliffhall Sep 26, 2026
c6baeba
fix(cli): classify auth errors by type, not message keywords; isolate…
BobDickinson Sep 26, 2026
e044bcd
ci: order release runs and re-assert the tarball's name and version
cliffhall Sep 26, 2026
c8aa0b2
fix(docker): skip the web healthcheck probe under --cli/--tui (#2415)
cliffhall Sep 26, 2026
07ffa39
ci: state the release concurrency group's actual guarantee
cliffhall Sep 26, 2026
e225600
Merge pull request #2509 from modelcontextprotocol/v2/fix/2485-gcm-au…
cliffhall Sep 26, 2026
bbfa221
fix(web): report the Inspector version as clientInfo.version
cliffhall Sep 26, 2026
6925180
fix(docker): only recognize the launcher in the two real PID 1 shapes
cliffhall Sep 26, 2026
30a9a89
Merge pull request #2506 from modelcontextprotocol/v2/chore/2483-spli…
cliffhall Sep 26, 2026
e35a0b0
fix(docker): require the node interpreter and keep empty argv entries
cliffhall Sep 26, 2026
51c7148
SHA-pin the actions in credential-holding workflow jobs
cliffhall Sep 26, 2026
6d06abd
test: stop the useServers render loop and guard against its return
cliffhall Sep 26, 2026
fe3872c
docs(auth): correct StoredOAuthClientInformation provenance comment
BobDickinson Sep 26, 2026
5e5b092
Merge pull request #2511 from modelcontextprotocol/v2/fix/2445-web-cl…
cliffhall Sep 26, 2026
2db4636
verify:action-pins: catch every secret spelling and reusable-workflow…
cliffhall Sep 26, 2026
f98e342
verify:action-pins: transitive artifact chains, aliases, default-only…
cliffhall Sep 26, 2026
8d83c83
fix(storage): restore non-__proto__ Object.prototype names as valid s…
BobDickinson Sep 26, 2026
1527a46
Merge pull request #2513 from modelcontextprotocol/v2/fix/2508-useser…
cliffhall Sep 26, 2026
be98965
test(web): configure React's act environment and wrap the updates it …
cliffhall Sep 26, 2026
cc80743
verify:action-pins: count secrets in the workflow-level env
cliffhall Sep 26, 2026
becc8c9
Merge pull request #2510 from modelcontextprotocol/v2/fix/2415-docker…
cliffhall Sep 26, 2026
084b838
Merge pull request #2514 from modelcontextprotocol/v2/chore/2507-reac…
cliffhall Sep 26, 2026
64a50d6
Merge pull request #2512 from modelcontextprotocol/v2/chore/2484-sha-…
cliffhall Sep 26, 2026
752eea6
fix(cli): rethrow permanent read failures at the --wait-for-auth dead…
BobDickinson Sep 26, 2026
2d85cea
fix(auth): compensate partial secret-store commits before degrading
BobDickinson Sep 26, 2026
1205d7d
fix(auth): keep prior residue on memory-only degrade; sweep rename de…
BobDickinson Sep 26, 2026
67ea28a
Merge remote-tracking branch 'origin/v2/main' into v2/feat/2481-oauth…
BobDickinson Sep 26, 2026
d45cb27
test: update degrade expectation for entry-level all-or-nothing revert
BobDickinson Sep 26, 2026
cabb007
test: fix semantic merge conflict in unredacted-classification test
BobDickinson Sep 26, 2026
bcaf4a2
fix(cli): bound --wait-for-auth reads by the wait deadline
BobDickinson Sep 26, 2026
944dc32
fix(auth): reject non-string secret values before they poison the store
BobDickinson Sep 26, 2026
ba3c5b5
fix(auth): share load/persist coordination per path; pin remote backe…
BobDickinson Sep 26, 2026
3abec6a
fix(auth): verify OAuth sectioned writes converge, re-applying over i…
BobDickinson Sep 26, 2026
311c615
fix(auth): restore to a fold-rule baseline on failed sectioned writes
BobDickinson Sep 26, 2026
ca122e3
docs(spec): double-failure store/file mismatch persists, not self-heals
BobDickinson Sep 26, 2026
d542c83
fix(auth): unify failure reconciliation and validate persisted token …
BobDickinson Sep 26, 2026
b560321
fix: keep unservable token payloads plaintext; serve them as no tokens
BobDickinson Sep 26, 2026
f5b232a
docs: residue is not unconditionally secret-free
BobDickinson Sep 26, 2026
6b7c8ea
fix: store partial token payloads instead of keeping them plaintext
BobDickinson Sep 26, 2026
a1914bc
fix: persist only changed secret fields on section saves
BobDickinson Sep 26, 2026
fc55e11
fix: serialize client.json combined writers and make bulk reads proto…
BobDickinson Sep 26, 2026
102344f
Merge branch 'v2/main' into v2/docs/ai-software-factory
cliffhall Sep 27, 2026
eb25cb4
Merge pull request #2482 from modelcontextprotocol/v2/feat/2481-oauth…
BobDickinson Sep 27, 2026
567c285
docs: apply Cliff's review corrections on #2498
BobDickinson Sep 27, 2026
f8aad8f
Merge remote-tracking branch into v2/docs/ai-software-factory
BobDickinson Sep 27, 2026
ad9fbb0
Merge pull request #2498 from modelcontextprotocol/v2/docs/ai-softwar…
BobDickinson Sep 27, 2026
a512fc2
fix(deps): make @modelcontextprotocol/server-legacy a devDependency
cliffhall Sep 28, 2026
1716af1
Merge pull request #2520 from modelcontextprotocol/v2/fix/2519-server…
cliffhall Sep 28, 2026
d30a31c
chore(deps): upgrade the MCP TypeScript SDK to 2.2.0
cliffhall Sep 30, 2026
4422f8d
chore(deps): upgrade the MCP Apps extension SDK to 2.0.3
cliffhall Sep 30, 2026
90704f3
Merge pull request #2526 from modelcontextprotocol/v2/chore/2521-sdk-…
cliffhall Sep 30, 2026
7ab8c5c
Merge pull request #2527 from modelcontextprotocol/v2/chore/2522-ext-…
cliffhall Sep 30, 2026
32c783f
fix(deps): raise undici to ^8.11.2 for ten high-severity advisories
cliffhall Sep 30, 2026
921c8a4
fix(deps): refresh ip-address to 10.7.2 for three moderate advisories
cliffhall Sep 30, 2026
8d6f9f9
fix(deps): refresh fast-uri to 3.1.8 for a moderate advisory
cliffhall Sep 30, 2026
b30ff27
fix(deps): refresh brace-expansion to 5.0.12 in root, web and tui
cliffhall Sep 30, 2026
754689d
chore(release): bump version to 2.9.0
cliffhall Sep 30, 2026
c60fd53
Merge pull request #2529 from modelcontextprotocol/v2/chore/2528-bump…
cliffhall Sep 30, 2026
e78127a
chore: merge v2/main into main for the v2.9.0 release
cliffhall Sep 30, 2026
7740bed
fix(core): redact a form secret's raw-& tail in recorded bodies
cliffhall Sep 30, 2026
b50369d
fix(cli): catch an opener that cannot be spawned instead of crashing
cliffhall Sep 30, 2026
e5b0f7b
fix(cli): call open from a queued microtask so the listener cannot lag
cliffhall Sep 30, 2026
e4e1c77
fix(core): fold a secret's tail across an empty segment
cliffhall Sep 30, 2026
6df9094
Merge pull request #2534 from modelcontextprotocol/v2/fix/2531-cli-op…
cliffhall Sep 30, 2026
df9d348
Merge pull request #2535 from modelcontextprotocol/v2/fix/2532-redact…
cliffhall Sep 30, 2026
36f3d81
chore: merge v2/main into main for the v2.9.0 release
cliffhall Sep 30, 2026
66dae5c
fix(cli): split trailing punctuation off redacted URLs in linear time
cliffhall Sep 30, 2026
7e606f5
Merge pull request #2541 from modelcontextprotocol/v2/fix/2540-linear…
cliffhall Sep 30, 2026
87bb185
chore: merge v2/main into main for the v2.9.0 release
cliffhall Sep 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 191 additions & 17 deletions .claude/skills/pr-flow/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
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 open, create or submit a PR; when a DCO or signoff check fails; when requesting a Copilot review or responding to review comments; when naming a branch; when attaching screenshots to a PR; or when closing out after a merge.
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
---

Expand All @@ -21,7 +21,69 @@ 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.

Move the issue's card to **In Progress** (`/board-ops`).
**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 whole block. It is the assignment, the card move, and a check; **the
step is done only when the last line prints `card: In Progress`.**

```sh
N=<ISSUE_NUMBER>; STATUS="In Progress"
BOARD=28 # 11 for a v1 issue — board #11 has the same column names
ASSIGNED=
gh issue edit "$N" --repo modelcontextprotocol/inspector --add-assignee @me \
&& ASSIGNED=1 || echo "assignment failed — this step is NOT done" >&2

# Every id is resolved BY NAME at run time, so none is copied from /board-ops
# and an option recreated after a deletion (its hazard) still resolves.
PROJECT_ID= FIELD_ID= OPTION_ID= ITEM_ID= # no id survives a failed lookup
PROJECT_ID=$(gh project view "$BOARD" --owner modelcontextprotocol --format json --jq .id)
FIELDS=$(gh project field-list "$BOARD" --owner modelcontextprotocol --format json) &&
FIELD_ID=$(jq -r '.fields[] | select(.name=="Status") | .id' <<<"$FIELDS") &&
OPTION_ID=$(jq -r --arg s "$STATUS" '.fields[] | select(.name=="Status")
| .options[] | select(.name==$s) | .id' <<<"$FIELDS")
# The card is found from the issue, not from a board listing (see /board-ops).
card() {
gh api graphql -F n="$N" -f query='query($n:Int!){
repository(owner:"modelcontextprotocol",name:"inspector"){issue(number:$n){
projectItems(first:100){nodes{id project{id}
fieldValueByName(name:"Status"){... on ProjectV2ItemFieldSingleSelectValue{name}}}}}}}' \
| jq -r --arg p "$PROJECT_ID" '.data.repository.issue.projectItems.nodes[]
| select(.project.id==$p) | "\(.id) \(.fieldValueByName.name // "(none)")"'
}
ITEM_ID=$(card | cut -d' ' -f1)
if [ -n "$PROJECT_ID" ] && [ -n "$FIELD_ID" ] && [ -n "$OPTION_ID" ] && [ -n "$ITEM_ID" ]; then
gh project item-edit --project-id "$PROJECT_ID" --id "$ITEM_ID" \
--field-id "$FIELD_ID" --single-select-option-id "$OPTION_ID" >/dev/null
else
echo "lookup failed (project='$PROJECT_ID' field='$FIELD_ID' option='$OPTION_ID' item='$ITEM_ID') — nothing edited" >&2
fi
NOW=$(card | cut -d' ' -f2-)
[ "$NOW" = "$STATUS" ] && [ -n "$ASSIGNED" ] && echo "card: $NOW" \
|| echo "card is '$NOW', assigned='${ASSIGNED:-no}' — this step is NOT done" >&2
```

An issue with no card on board `$BOARD` fails the lookup; board it there first with
`/issue-create`'s card step rather than skipping the move.

## 2. Branch

Expand Down Expand Up @@ -218,12 +280,77 @@ gh pr create --repo modelcontextprotocol/inspector \
**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`.
There is no `gh` flag for manual linking; closing keywords are the only
mechanism GitHub exposes.

Move the card to **In Review**.
**So link the PR to its issue explicitly, right after creating it.** The
`addCloseIssueReferences` GraphQL mutation adds a manual closing reference, the
same link as the UI's **Development** sidebar, and it works whatever the base
branch. 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.

## 7. Request a Copilot review
```sh
ISSUE_ID=$(gh api graphql -F n=<ISSUE_NUMBER> -f query='query($n:Int!){
repository(owner:"modelcontextprotocol",name:"inspector"){issue(number:$n){id}}}' \
--jq .data.repository.issue.id)
PR_ID=$(gh pr view <N> --repo modelcontextprotocol/inspector --json id --jq .id)
gh api graphql -f query='mutation($i:ID!,$p:[ID!]!){
addCloseIssueReferences(input:{issueId:$i, pullRequestIds:$p}){clientMutationId}}' \
-f i="$ISSUE_ID" -f p="$PR_ID"

# Verify: the PR should list the issue.
gh api graphql -F n=<N> -f query='query($n:Int!){
repository(owner:"modelcontextprotocol",name:"inspector"){pullRequest(number:$n){
closingIssuesReferences(first:10){nodes{number}}}}}' \
--jq '[.data.repository.pullRequest.closingIssuesReferences.nodes[].number]'
```

The link does not change how the issue closes on a v2 merge; that is still
step 9. `removeCloseIssueReferences` 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`.** It is step 1's block with a different
column and no assignment. Run it in full and check that the last line prints
`card: In Review`:

```sh
N=<ISSUE_NUMBER>; STATUS="In Review" # the ISSUE number, not the PR's
BOARD=28 # 11 for a v1 issue — board #11 has the same column names

PROJECT_ID= FIELD_ID= OPTION_ID= ITEM_ID= # no id survives a failed lookup
PROJECT_ID=$(gh project view "$BOARD" --owner modelcontextprotocol --format json --jq .id)
FIELDS=$(gh project field-list "$BOARD" --owner modelcontextprotocol --format json) &&
FIELD_ID=$(jq -r '.fields[] | select(.name=="Status") | .id' <<<"$FIELDS") &&
OPTION_ID=$(jq -r --arg s "$STATUS" '.fields[] | select(.name=="Status")
| .options[] | select(.name==$s) | .id' <<<"$FIELDS")
card() {
gh api graphql -F n="$N" -f query='query($n:Int!){
repository(owner:"modelcontextprotocol",name:"inspector"){issue(number:$n){
projectItems(first:100){nodes{id project{id}
fieldValueByName(name:"Status"){... on ProjectV2ItemFieldSingleSelectValue{name}}}}}}}' \
| jq -r --arg p "$PROJECT_ID" '.data.repository.issue.projectItems.nodes[]
| select(.project.id==$p) | "\(.id) \(.fieldValueByName.name // "(none)")"'
}
ITEM_ID=$(card | cut -d' ' -f1)
if [ -n "$PROJECT_ID" ] && [ -n "$FIELD_ID" ] && [ -n "$OPTION_ID" ] && [ -n "$ITEM_ID" ]; then
gh project item-edit --project-id "$PROJECT_ID" --id "$ITEM_ID" \
--field-id "$FIELD_ID" --single-select-option-id "$OPTION_ID" >/dev/null
else
echo "lookup failed (project='$PROJECT_ID' field='$FIELD_ID' option='$OPTION_ID' item='$ITEM_ID') — nothing edited" >&2
fi
NOW=$(card | cut -d' ' -f2-)
[ "$NOW" = "$STATUS" ] && echo "card: $NOW" || echo "card is '$NOW', not '$STATUS' — this step is NOT done" >&2
```

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
Expand All @@ -239,17 +366,21 @@ gh api graphql -f query='
}' -f pr="$PR_ID" -f bot='BOT_kgDOCnlnWA'
```

Poll for the review with a `startswith` match — the review login carries a
`[bot]` suffix. **Put that poll in one backgrounded loop that exits when the
round lands, 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) — and exactly where the
poll belongs when one is needed.
### 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. **Put it in one backgrounded
loop that exits when the round resolves, 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
EXPECTED=1 # the review COUNT you are waiting to reach — see below
while :; do
DEADLINE=$(( $(date +%s) + 1500 )) # 25 min; rounds normally land in 2–10
count() {
# Capture first, so a gh failure stops the loop instead of being swallowed by
# a pipeline. --slurp cannot be combined with --jq, hence the separate jq.
raw=$(gh api --paginate --slurp \
Expand All @@ -258,7 +389,21 @@ while :; do
n=$(jq '[.[][] | select(.user.login | startswith("copilot-pull-request-reviewer"))] | length' <<<"$raw") || {
echo "jq failed ($?) on an unexpected response shape" >&2; exit 1; }
case $n in '' | *[!0-9]*) echo "not a count: '$n'" >&2; exit 1 ;; esac
[ "$n" -ge "$EXPECTED" ] && break
}
pending() {
p=$(gh api graphql -f query='{repository(owner:"modelcontextprotocol",name:"inspector"){pullRequest(number:<N>){reviewRequests(first:20){nodes{requestedReviewer{... on Bot{login} ... on User{login}}}}}}}' \
--jq '[.data.repository.pullRequest.reviewRequests.nodes[].requestedReviewer.login // empty | select(test("copilot";"i"))] | length') || {
echo "gh graphql failed ($?)" >&2; exit 1; }
}
while :; do
count; [ "$n" -ge "$EXPECTED" ] && { echo "ROUND=posted"; break; }
pending
if [ "$p" = 0 ]; then
sleep 30; count # the request can clear a beat before the review is visible
[ "$n" -ge "$EXPECTED" ] && echo "ROUND=posted" || echo "ROUND=ended-without-review"
break
fi
[ "$(date +%s)" -ge "$DEADLINE" ] && { echo "ROUND=timed-out"; break; }
sleep 30
done
```
Expand All @@ -272,8 +417,37 @@ read as a count of `0`; and a `jq` failure on an unexpected shape leaves `n`
empty, whereupon `[ "" -ge 1 ]` exits non-zero, `break` never fires, and the job
sleeps and retries forever — the same unbounded wait, reached from the other
end. A background task that can never succeed is worse than one that never
started, because it looks like progress. Give the inline comments a further ~60s after the body lands; they
arrive late (see step 8).
started, because it looks like progress. 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 |
| --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| had an in-scope finding you fixed and pushed | Request another round (7a), `EXPECTED` + 1. |
| was clean — no inline comments, nothing in the body headline or `Suppressed comments` | **Stop.** One clean round is the end — never request a confirming round "just to be sure"; it spends Copilot tokens to re-review code nothing has changed. |
| held only findings you declined as out of scope (see below) | **Stop.** Nothing changed, so another round only re-argues the same scope. |
| `ended-without-review` | Request once more. Two in a row means Copilot's session on this PR has ended — stop. |
| `timed-out` | **Stop and report the round as still pending.** The request is still open, so re-running `requestReviews` for the same bot is a no-op and starts nothing new. |

"Clean" means all three channels are empty — inline comments, the body's
headline sentence, and the `Suppressed comments` block. A zero-comment round
can still name a real bug in the headline or the suppressed block — read all
three before calling it clean.

**Weigh every finding against the issue the PR closes.** Fix what is a defect
_in what this PR added_. Decline, with a reason in the thread, anything that is
pre-existing behavior, a new capability, or hardening beyond what the issue
asks for — Copilot does not converge on its own, and every fix it talks you
into beyond the issue is fresh surface for the next round, so accepting scope
creep is what makes a review cycle protracted. If a declined finding is a real
problem worth doing, file it with `/issue-create` and link it in the reply
rather than growing the PR.

When the loop stops, post a PR-level comment saying the review is closed and
why (which exit fired), and report the same in your reply to the user.

## 8. Respond to the review

Expand Down
12 changes: 12 additions & 0 deletions .claude/skills/pr-flow/evals/evals.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
[
{
"prompt": "create a PR for #2463",
"expect": "pr-flow"
},
{
"prompt": "Create a PR for #2381.",
"expect": "pr-flow"
},
{
"prompt": "I've finished the fix for issue 2071. Take it through to a pull request.",
"expect": "pr-flow"
Expand All @@ -19,6 +27,10 @@
"prompt": "My PR is open and green. Walk me through getting it merged and closed out here.",
"expect": "pr-flow"
},
{
"prompt": "Open the PR for #2400, then keep getting Copilot to review it until it has nothing left to say.",
"expect": "pr-flow"
},
{
"prompt": "What does this regex match? /^[a-z]+$/",
"expect": null
Expand Down
44 changes: 35 additions & 9 deletions .claude/skills/pre-push-gate/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ prints each stage as it starts, so the running command is the other reliable
answer.

It runs **every check** GitHub CI runs (which additionally runs `npm install`,
and runs `coverage` as a parallel job), plus two local-only steps. So the
and runs `coverage` as a parallel job), plus one local-only step. So the
direction that matters holds: **passing `local:gate` locally means every check
CI applies has already passed on your machine** — the strongest predictor of a
green CI there is here, though not a proof (a different OS, and the bare test
Expand Down Expand Up @@ -89,6 +89,32 @@ model-invoked skill is missing its eval cases. `verify:skills:cli` is the
authoritative validator and fetches a pinned CLI over the network if you have
none installed — so it is also the one stage that will fail offline.

### `verify:install-fresh`

An installed package's version disagrees with its install's lockfile — `node_modules`
is older than the tree you pulled. **Run `npm install` at the repo root** (it
cascades into every client) and re-run. This is the first guard for a reason: a
stale install otherwise passes every check and fails later as a behavioral test
reporting the *old* dependency's behavior as a product bug (#2494). Don't
"fix" that test.

### `verify:action-pins`

A job that holds a credential (`id-token`/`packages: write`, a non-default
secret, or it builds an artifact such a job downloads) runs an action that is
not SHA-pinned (#2484). Pin it the way its neighbours are —
`owner/repo@<40-hex sha> # vX.Y.Z` — resolving both from one lookup:

```sh
REPO=actions/checkout; TAG=v7
SHA=$(gh api "repos/$REPO/commits/$TAG" --jq .sha)
gh api --paginate "repos/$REPO/tags?per_page=100" \
--jq ".[] | select(.commit.sha==\"$SHA\") | .name" | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1
```

If a job started failing because it gained a secret or a scope, that is the
guard doing its job — pin its actions rather than dropping the scope to dodge it.

### `verify:dep-lockstep`

A dependency reaching one `tsc` program from two installs resolves to two
Expand Down Expand Up @@ -165,13 +191,15 @@ and a `pgrep -f "npm run local:gate"` loop matches _itself_ and never exits.
A gate that starts with

```
gate-lease: pid 12345 in /Users/you/Projects/mcp-inspector-wt-1, running for 2m10s holds the gate lease; waiting …
gate-lease: pid 12345 in /Users/you/Projects/mcp-inspector-wt-1, running for 2m10s holds the gate lease, with 2 more gates queued ahead of this one; waiting …
```

is queued behind another worktree's gate, and will start the moment it
releases (it re-checks every 2s and prints `still waiting` once a minute). The
is queued behind another worktree's gate. Queued gates start in the order they
arrived (#2473), so this one starts once the holder and the gates ahead of it
have run (it re-checks every 2s and prints `still waiting` once a minute). The
holder's pid and worktree are in the line, so you can decide whether to wait
or to stop that gate. A holder that was **killed** — a closed terminal, an
or to stop that gate. A queued gate that is stopped or killed while waiting
leaves the line at the next waiter's poll; nothing needs cleaning up. A holder that was **killed** — a closed terminal, an
OOM'd session — stops refreshing its lock and is taken over after 30s; nothing
needs cleaning up by hand. The one exception is a dead holder's lock directory
that cannot be removed (a stray file inside it, or permissions): the takeover
Expand All @@ -187,16 +215,14 @@ own.
for a measurement that needs contention; it does not get a result sooner,
because the queued run finishes before an overlapped one would.

## Local-only steps
## Local-only step

Two stages have no GitHub CI counterpart, each deliberately:
One stage has no GitHub CI counterpart, deliberately:

- **`smoke:web:firefox`** — the three browser-driven web smokes again under
Firefox. Trialled as a CI job and removed (#2086): across a dozen runs it never
disagreed with Chromium, and `playwright install --with-deps` carries a real
flake surface. Kept in front of a human about to push instead.
- **`smoke:tui`** — needs a real TTY. It _is_ invoked in CI via `npm run smoke`
and self-skips there on `process.env.CI`, so it needs no guarding.

A guard (`scripts/lib/workflow-gate.mjs`, run by `npm run test:scripts`) fails
the suite if a workflow invokes a `local:*` script, a non-Chromium engine pass,
Expand Down
Loading
Loading