Skip to content

chore: merge v2/main for the v2.1.0 milestone release - #1901

Closed
cliffhall wants to merge 14 commits into
mainfrom
v2/main
Closed

chore: merge v2/main for the v2.1.0 milestone release#1901
cliffhall wants to merge 14 commits into
mainfrom
v2/main

Conversation

@cliffhall

Copy link
Copy Markdown
Member

Closes #1876

The first v2/mainmain milestone merge. Brings the v2.1.0 payload — 14 commits — onto the release branch.

⚠️ This PR currently has 8 conflicts, and #1876's "expect ZERO conflicts" prediction is stale. Read the section below before merging; the resolution path matters, because the obvious one re-introduces the lineage problem #1868 was closed to avoid.

What's in it

Full grouped breakdown in #1876's milestone overview comment. In brief:

Group PRs
Security & dependency hardening #1899 (GHSA-p63j-vcc4-9vmv critical, @vitest/* → 4.1.10), #1900 (eslint 10, clears the brace-expansion DoS)
Tree-swap recovery #1869 (Claude workflow + .mcp.json), #1871 (dependabot, .gitattributes, CoC), #1867 (SECURITY.md)
Contribution model & branch policy #1866, #1879, #1884, #1894 (issue forms)
Board & triage process #1892 (Incoming status, Priority field, triage rubric)
Protocol correctness #1847the only runtime behavior change in the milestone
Merge convergence #1880, #1881
Tooling reliability #1895

User-visible surface is small: #1847 (tools/call now sends the SEP-2243 Mcp-Param-* headers, fixing rejection by strict modern servers), the Node engine floor moving >=22.7.5>=22.19.0, and the issues-only contribution model (#1894, #1884). Everything else is repo infrastructure a consumer never sees.

The conflicts, and why the prediction was stale

#1876 measured the conflict set when the only divergence was the README, and concluded #1880 would reduce it to zero. Since then main received 8 post-swap commits that v2/main never took#1843 (SECURITY.md restored), #1842 (2.0.0), #1841 (vite 8.1.5), #1837 (dep sweep), #1836, #1835, #1834, #1832, #1831 — and on the other side #1899 and #1900 regenerated every lockfile. So:

File Conflict Cause
README.md content main's restructure vs. #1880's convergence
SECURITY.md add/add restored independently on both sides — #1843 on main, #1867 on v2/main
package-lock.json (root) content 2.0.0 bump + dep sweep vs. eslint 10 regeneration
clients/{web,cli,tui,launcher}/package-lock.json content same, four times
clients/web/package.json content main's rc-series deps vs. #1899/#1900's resolved set

None is semantically hard — the lockfiles should be regenerated from a clean npm install, not hand-merged, and for SECURITY.md / README.md / clients/web/package.json the v2 side is authoritative (it is the newer content, and #1880/#1881 exist specifically to converge them).

⚠️ Do not resolve these with GitHub's web "Resolve conflicts" button

That editor commits the resolution to the head branch — i.e. it merges main into v2/main. That is precisely the back-merge that #1868 was closed to avoid, because it drags main's pre-swap v1 lineage into the develop branch's ancestry permanently.

The clean path, which keeps v2/main untouched:

git fetch origin
git checkout -b v2/chore/milestone-merge-v2.1.0 origin/main
git merge --no-ff origin/v2/main      # resolve here, on a throwaway branch
#   → lockfiles: take v2/main's, then regenerate with a root `npm install`
#   → SECURITY.md / README.md / clients/web/package.json: v2/main's side
npm install && npm run ci             # gate it before it becomes the release
git push origin v2/chore/milestone-merge-v2.1.0

…then retarget this PR (or open one from that branch) into main. main's ruleset requires a PR either way — see below.

Merge mechanics (main is ruleset-protected)

Classic branch protections are not configured, which is misleading: two active rulesets target ~DEFAULT_BRANCH, and one of them (2749948) grants bypass to nobody, repo admins included.

  • A PR is mandatorygit push origin main will be rejected.
  • 1 approving review required, from someone other than the author. A Copilot approval does not count.
  • The build check must pass.
  • allowed_merge_methods includes merge, so the intended --no-ff merge commit is permitted — merge via the button / gh pr merge --merge, not a local merge + push.
  • npm version cannot be pushed to main either, for the same reason. Fold the version bump into the merge branch, or follow up with a second PR.
  • Not blockers, verified: require_code_owner_review is inert (no CODEOWNERS anywhere), and the release tag is unrestricted (the only tag ruleset matches refs/tags/1.*).

Note on Closes #1876

main is the default branch, so that keyword will auto-close #1876 on merge. Its checklist still has post-merge items (cut the release, confirm @claude works again, confirm Dependabot targets v2/main, release notes) — reopen it after merging, or move those items out first.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

SamMorrowDrums and others added 14 commits July 28, 2026 19:19
…call (#1847)

* fix(core): mirror SEP-2243 x-mcp-header args to Mcp-Param-* on tools/call (#1846)

A strict modern (2026-07-28) HTTP server (e.g. the GitHub MCP Server)
rejects a tools/call whose argument carries an `x-mcp-header` annotation
when the matching `Mcp-Param-*` request header is missing (-32020
"header mismatch"). The Inspector never sent that header.

The SDK only mirrors inside `client.callTool()` (and skips it entirely in
a browser environment), but the Inspector routes tools/call through
`client.request()` for manual MRTR driving (#1704), so the SDK never
mirrors for us — on any transport. On top of that, the remote (web)
transport dropped per-request headers before they reached the backend's
upstream fetch.

- core/json/xMcpHeader.ts: port the SDK's non-public `buildMcpParamHeaders`
  + value encoding (Base64 sentinel for non-ASCII/whitespace/empty values)
  and add `mcpParamHeadersForTool`.
- InspectorClient.attemptToolCall: on a modern connection, build the
  headers from the tool's declarations + call arguments and attach them to
  the tools/call request options. `Protocol.request` forwards `headers`
  to the transport and preserves them across MRTR retry legs, so the
  direct StreamableHTTP transport applies them. Fixes CLI/TUI.
- RemoteClientTransport.requestSend + /api/mcp/send + RemoteSendRequest:
  forward per-send headers and apply them to the backend's upstream
  transport.send. The browser can't set cross-origin headers, but the
  Node backend can — where the Inspector's real request is issued. Fixes web.

Verified end-to-end against the live GitHub MCP Server (modern HTTP).

Closes #1846

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 36dbeb09-12b0-4c19-af36-2573772a7dbd

* review: harden backend header allowlist, fix breadcrumbs, trim comments

Address Copilot review on #1847:
- server.ts: restrict client-supplied per-send headers to the `Mcp-Param-`
  prefix (string values only) via `mcpParamHeadersOnly` before applying them
  upstream, so the per-send channel can't inject arbitrary headers (e.g.
  Authorization). `settings.headers` remains the channel for configured
  upstream headers. Covered by new unit tests.
- Fix stale `#1810` breadcrumbs → `#1846` in the two new tests.
- Trim the verbose mirroring comments in inspectorClient/types/remote transport.
- Drop the redundant base64-on-the-wire integration test (encoding is unit-
  tested; wire delivery is proven by the verbatim test), remove two unneeded
  test casts, and cover the boolean `true` branch.

Not changed (declined with reason on the PR): buildMcpParamHeaders remains a
faithful port of the SDK's helper and does not re-validate values against the
declared JSON Schema type — arg types are enforced by schema validation on
both sides, and matching the SDK's conforming wire output is the module's goal.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 36dbeb09-12b0-4c19-af36-2573772a7dbd

* review: mirror Mcp-Param-* on the task-augmented tools/call too

Address the maintainer review on #1847:

- `callToolStream` (the "Run as task" path) built its own request options and
  never mirrored, so a task-augmented `tools/call` on an annotated tool was
  still rejected by a strict modern server with -32020. It now mirrors like the
  plain path.
- Extract the two blocks both `tools/call` entry points had duplicated —
  `convertStringToolArgs` (the string-arg coercion) and
  `applyMirroredParamHeaders` (the SEP-2243 mirroring) — so the paths can't
  drift again. That duplication is why the task path was missed.
- Regression test: `callToolStream` against the modern `get_weather` server
  asserts the POST carries `Mcp-Param-City` (verified failing without the fix).
- README: the modern-network showcase claimed mirroring is skipped in the
  browser so the web client gets -32020, and that CLI/TUI mirror. Both are now
  wrong — the Inspector mirrors itself, on every client. AGENTS: note that
  xMcpHeader.ts now also builds the wire headers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UkhhCHryVp5H3dqEnhwZ7t

* docs: correct the stale "browser omits Mcp-Param-*" copy

Both statements predate this PR and are now the opposite of true, per the
wire capture on #1847 (proxy-observed upstream headers, web leg included).

- ToolDetailPanel: the mirrored-headers note claimed the browser omits
  `Mcp-Param-*`. It now says the web client's headers are applied by the Node
  backend that issues the upstream request.
- createRemoteFetch: the header comment claimed an `x-mcp-header`-annotated
  tool is "uncallable from the web client against a strict modern server" —
  the exact wrong conclusion to reach while debugging a -32020. It now records
  that the Inspector builds the headers itself, and that they do NOT travel
  this `/api/fetch` proxy path: they ride the transport's per-send `headers`
  through `/api/mcp/send`.

Copy only — no test asserted the old note text (ToolDetailPanel.test.tsx
matches the section title only).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UkhhCHryVp5H3dqEnhwZ7t

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: cliffhall <cliff@futurescale.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot-Session: 36dbeb09-12b0-4c19-af36-2573772a7dbd
…review instructions (#1866)

* docs: correct post-swap branch model, PR policy, and stale gate/tree entries

Part of the go-live phase-8 docs hygiene: everything that silently inverted
when `main` became v2.

Branch/release model — documented canonically in AGENTS.md and corrected
everywhere it was restated:
- AGENTS.md: replace the one-line "Base Branches" bullet with a table giving
  each branch its role (v2/main = develop, main = release, v1/main =
  maintenance), whether PRs target it, and which npm dist-tag it publishes.
- README.md: the "Repo status" callout had it backwards, claiming `main` was
  the legacy v1 implementation. Also note that a v2 release is cut from `main`
  after the milestone merge, not from v2/main.
- CONTRIBUTORS.md: the version table listed v2's base branch as `main`.

PR policy:
- Every PR must reference an issue, from anyone, no exceptions.
- PRs are opened by repo maintainers only — org write access is not
  authorization. Everyone else files a detailed issue.
- Branch names start with the target version segment (v2/…, v1/…).
- Issues and PRs carry exactly one of `v1`/`v2` at creation; default `v2`.
- Screenshots proving UI/TUI changes go in the gitignored pr-screenshots/.

Corrections to stale content found while in there:
- The TUI coverage bullet still described the Ink/App.tsx/hooks exclusion as
  interim pending #1501, which closed 2026-06-29; the gate now covers all of
  src/** with only tui-servers.ts excluded.
- The completion checklist said `npm run validate` was the gate and covered
  "e2e tests", contradicting the mandatory-pre-push-gate section (npm run ci).
- v1 PR base was given as `main`; it is `v1/main`.
- Project tree omitted core/client, docs/, and scripts/ (README: core/client,
  docs/). core/client is under the coverage gate and imported by both App.tsx
  trees.

Also adds the launcher and `npm run test:scripts` to the "run the tests" list
and a tiers overview, since neither was mentioned.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

* docs: require a milestone on every new issue, defaulting to the current one

Adds the rule alongside the version-label rule, since they fail the same way:
an unmilestoned issue drops out of release planning as silently as an
unlabeled one drops out of version filtering.

- Set it at create time (`gh issue create --milestone <title> ...`).
- If the user didn't specify one, default to the current milestone — the open
  milestone with the nearest due date — rather than leaving it blank pending a
  decision. Includes the `gh api .../milestones` one-liner that identifies it.
- Milestones are release buckets, so pick by when the work ships, not by size;
  sub-issues normally inherit their parent's milestone.

Also widens the section's opening callout: an issue is not "created" until it
is labeled, milestoned, AND boarded with a Status — four distinct steps, not
three.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

* docs: add .github/copilot-instructions.md and require mirroring into it

GitHub Copilot reads .github/copilot-instructions.md when reviewing a PR. The
repo had no such file, so Copilot reviewed without any of the conventions that
actually govern this codebase — the ban on `any` and on unjustified double
casts, the Mantine-first styling rules, the `.withProps()` extraction rule, the
lib/utils split, test placement, and the per-file >=90% four-dimension gate.

The new file is a review-focused distillation of AGENTS.md: the rules a
reviewer would cite against a diff, plus a short "what to prioritize" list that
puts correctness and security first (this backend spawns processes and proxies
outbound requests). Deliberately omitted: board recipes and IDs, milestone and
branch mechanics, release procedure, and the project tree — no reviewer cites
those, and copying them would double the maintenance surface.

AGENTS.md's "Keep documentation files up to date" section now requires
mirroring review-relevant changes into it in the same PR, states that AGENTS.md
stays the source of truth, and spells out what counts as review-relevant. There
is no generation step and nothing detects drift, so the rule is the only thing
keeping the two in sync.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

* docs: address Copilot review — v1 policy wording, stale V2 Go Live, typo

- Align the v1 maintenance policy on "security and bug fixes only" in
  AGENTS.md and CONTRIBUTORS.md; both said "security fixes only" while the
  README callout, the branch table, and the board description said
  "security and bug fixes".
- Drop the stale "or V2 Go Live" instruction from the work-begins step —
  that column no longer exists on board #28 (verified via
  `gh project field-list 28`), so the option id it implied would be
  rejected. Restore the removed-column id list the same edit dropped, now
  including V2 Go Live, and re-date the table to today's verification.
- Fix "contains the the new version" typo.
- Strip a stray trailing blank line at the end of AGENTS.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

* docs: restore `inspector/` as the Project Structure tree root

The tree is a filesystem layout — every child is a real directory
(`clients/`, `core/`, `test-servers/`). Labeling its root `v2/main/` named
a branch instead, which reads as a path that does not exist in a checkout
and diverged from the identical tree in README.md, which still says
`inspector/`. The branch model is documented in its own table under
Repository & Project Boards; the tree does not need to restate it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

* docs: settle the v1 policy wording as "security fixes only"

Reverses the direction the Copilot round took in 33cc036. That commit
normalized the six v1-policy sites onto "security and bug fixes" because that
was the majority wording — but majority is the wrong tiebreak here. The policy
is set by what we told contributors: the #1819 backlog close note, posted to
120 closed PRs, says "v1 will receive security fixes only", and the five PRs
retained for 1.0.5 are all security fixes.

So the two sites that already said "security fixes only" were the correct ones,
and the four that said otherwise are now aligned to them:

- AGENTS.md: the `v1` label description, the branch-role table, and the v1
  board line.
- README.md: the "Repo status" callout.

CONTRIBUTORS.md and AGENTS.md's Project Status bullet already read correctly
and are unchanged.

This is the substance of #1813, which is why that issue's PR is now reduced to
its PR-template-link half — the wording work landed here to avoid two PRs
editing the same lines in opposite directions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

* docs: point the AGENTS.md policy link at CONTRIBUTING.md

This PR rewrites the paragraph holding the only in-repo link to the
contributing policy, so it owns that line. #1884 renames the file
CONTRIBUTORS.md -> CONTRIBUTING.md; having #1884 also edit this line would
guarantee a one-line conflict between the two.

Writing the new filename here instead makes #1884 a pure `git mv` with no
overlapping line, so neither PR conflicts with the other.

Between this merging and #1884 merging, the link points at a filename that
doesn't exist yet. That window is deliberate and bounded — this PR is first in
the merge order (#1821) and #1884 is third.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…1880)

* docs: converge README Test servers section with main's restructure

Transplants main's restructured "Test servers" section (#1836) onto
v2/main — the "Serving the modern protocol era" and "Showcase configs"
headings, the config table, and the per-config subsections — replacing
the eight long paragraphs v2/main still carried.

Folds in v2/main's #1846 correction: main's copy still claims
Mcp-Param-* mirroring is skipped in the browser, which stopped being
true when the Inspector took over building the mirrored headers itself.
The blockquote callout keeps main's shape with the corrected wording.

Scoped to that section only — the whole file is deliberately NOT copied
from main, so #1866's repo-status callout and release-section edits (and
every other v2/main-only line) are untouched.

Shrinks the first v2/main -> main milestone merge from a whole-section
conflict to a single 3-line hunk.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

* docs: point the #1846 reference at the issue, not a nonexistent PR URL

#1846 is the issue; the fix shipped as PR #1847. The callout's link used
/pull/1846, which 404s. Per Copilot review.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e policy (#1884)

* docs: rename CONTRIBUTORS.md to CONTRIBUTING.md (#1883)

GitHub's contributing-guidelines banner — shown when someone clicks
"New issue" or opens a pull request — keys on the exact filename
CONTRIBUTING.md. Naming the file CONTRIBUTORS.md left that prompt dark
precisely where the "open an issue, not a PR" policy needs to land.

Renamed with `git mv` so history follows the file; contents are
unchanged (its heading already reads "Contributing to MCP Inspector").
Updated the one in-repo reference, in AGENTS.md. The two links in
.github/pull_request_template.md are deliberately untouched — PR #1879
makes them relative so they follow the rename.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

* docs: drop the AGENTS.md link edit; #1866 owns that line

This PR edited the `See [CONTRIBUTORS.md](./CONTRIBUTORS.md)` link in
AGENTS.md, which is the exact line #1866 rewrites — guaranteeing a one-line
conflict when this merges after it.

Handing that line to its owner instead: #1866 now writes the link as
`CONTRIBUTING.md` directly, so this PR is a pure `git mv` and the two touch no
common line. No conflict to resolve at merge time.

The tradeoff is a short window between the two merges where AGENTS.md links to
a filename that doesn't exist yet. That's deliberate and bounded: #1866 is
first in the merge order and this is third.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
#1879)

* docs: say "security fixes only" for v1 and unpin the PR-template links

The v1 maintenance policy was stated two different ways across the docs —
"security fixes only" in CONTRIBUTORS.md, "bug fixes" in README.md. The
#1819 PR triage told 120 contributors that v1 is security-fixes-only, so
that is the wording we keep.

Also make the PR template's two CONTRIBUTORS.md links relative instead of
pinned to blob/main/. `main` is release-only, so those resolved against
the last released tree rather than the branch the reader is on.

The remaining AGENTS.md occurrences are left alone deliberately: PR #1866
rewrites those exact lines and lands first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

* docs: drop the README change; #1866 owns that line

The "Repo status" callout edit here patched `(bug fixes only)` →
`(security fixes only)` on the callout as it exists on v2/main. But #1866
replaces that entire callout, because the surrounding sentence is false today:
it still says "The `main` branch is the legacy v1 implementation", when `main`
now holds the latest released v2.

So this edit both collides with #1866 on the same line and, taken on its own,
would preserve the false statement while correcting only the parenthetical.
#1866 already sets the v1 policy wording to "security fixes only" across
AGENTS.md and README (ee020ff), so nothing is lost by dropping it here.

This PR is now exactly its intended half: the two branch-pinned
`blob/main/CONTRIBUTORS.md` links in .github/pull_request_template.md, made
relative. That file is untouched by #1866, so the two PRs no longer overlap on
any line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…p it (#1867)

Closes #1864.

SECURITY.md was restored to main in #1843 but never added to v2/main.
Because v2/main is the develop branch merged into main at milestones, and
the two share no common git ancestor, the next milestone merge would
silently remove the security policy from the default branch a second time.

Copied byte-for-byte from main (sha256
bbc8aaa5a33d481f902c1b7ac260a4c9c0d41dac5bb2916607222dd205dda924,
2633 bytes). No content changes.


Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ee swap (#1869)

* ci: restore the Claude Code workflow, lost in the v2 tree swap

Closes #1850.

Restored byte-for-byte from ac3c1a1 (sha256
1c44f2d9d51c936adb40c9acefd4b44628fe7810360e2f8196034347f9d2602d,
3592 bytes). No content changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

* ci: restore .mcp.json, which claude.yml's --mcp-config points at

The restored workflow runs with `--mcp-config .mcp.json` and
`--allowedTools "Bash,mcp__mcp-docs"`, so without this file the mcp-docs
server never registers and those tools do not resolve.

Lost in the same tree swap: .mcp.json survives only on v1/main, and is
absent from both main and v2/main. Restored byte-for-byte from 47f9284
(sha256 b0f0b8ea0d6c6e4bd9d19270db8c4ed13a951c6a7d8fa9a7b76d0d12ddd2491f,
123 bytes). No content changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: restore repo files dropped by the v2 tree swap

A sweep of the pre-swap tree (ac3c1a1) against v2/main turned up 14 files the
tree swap dropped and that were never restored to either branch. Most are
correctly gone (v1 CI workflows, v1 sample-config, husky hook, two 0-byte
files, v1-specific prettier config). These four are worth having:

- .github/dependabot.yml — rewritten, not restored. v1's covered only
  github-actions at /; this covers github-actions plus the six npm manifests
  v2 actually has (root + four clients), monthly rather than weekly, grouped
  to one PR per ecosystem/directory, targeting v2/main, and labeled `v2` so
  the version-label rule holds for bot PRs too.
- .github/ISSUE_TEMPLATE/bug_report.md — rewritten. v1's asked for an
  "Inspector Version e.g. 0.16.5" and had no client or version field. Under
  the issues-only policy this template is the primary intake, so it now asks
  the routing questions first (v1 vs v2, which client) and points at the
  prompt-not-a-diff path.
- .gitattributes — restored byte-for-byte (package-lock.json
  linguist-generated=true). More useful in v2 than v1: there are now six
  lockfiles, and they dominate dependency-bump diffs.
- CODE_OF_CONDUCT.md — restored byte-for-byte, so GitHub surfaces it as a
  community standard. Left unformatted deliberately: it is verbatim upstream
  Contributor Covenant text, v1 listed it in .prettierignore for that reason,
  and no v2 format glob reaches root markdown.

CONTRIBUTING.md is deliberately not restored here — it is handled by renaming
CONTRIBUTORS.md to that name, which also lights up GitHub's contributing
banner without leaving two files to keep in sync.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

* docs(dependabot): state the documented target-branch/security-update rule

Copilot review: the comment read as if `target-branch` had no bearing on
security updates. Per GitHub's Dependabot options reference, an entry whose
`target-branch` names a non-default branch is not applied to security
updates at all — so the schedule/labels/groups here shape version updates
only, and security PRs continue to be raised against the default branch.

Comment-only; the configured settings are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

* docs: correct the branch description and the labeling ask

Two suppressed Copilot comments, both correct:

- dependabot.yml called `main` "release-only". It isn't — README.md and
  AGENTS.md both describe it as the legacy v1 implementation taking security
  and bug fixes only. Reworded to match, keeping the point that matters
  (Dependabot reads config from the default branch).
- bug_report.md asked reporters to label the issue `v2`/`v1`, which most
  external reporters have no permission to do. It now asks only for the
  version checkbox and says a maintainer labels it at triage.

Comment/prose only; no configured Dependabot setting changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

* docs(dependabot): describe main as release-only, not "legacy v1"

Reverts the branch description introduced in c7fbf35. That change was made to
match README.md:17 and AGENTS.md — but those are precisely the stale sentences
PR #1866 exists to correct. `main` has not been the legacy v1 implementation
since the go-live tree swap; it is the release branch holding the latest
released v2, and the deprecated v1 line lives on `v1/main`.

So the comment now describes the real model: v2/main is the develop branch,
main is release-only and receives milestone merges, v1/main is maintenance and
takes security fixes only.

The rest of c7fbf35 stands — the bug_report.md change (asking reporters for
the version checkbox rather than to apply a label they lack permission to set)
was a genuine catch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…8.1.5) (#1881)

The 2.0.0-rc series shipped these dependency bumps to `main` only, so
`v2/main` was developing against older versions and would have met them
for the first time at the milestone merge.

- `@hono/node-server` ^1.19.14 -> ^2.0.12 (root, clients/web) — a major.
  v2.0.0's only breaking changes are dropping Node 18 (the repo already
  requires >=22.19.0) and removing the `@hono/node-server/vercel` adapter
  (unused here). The used surface — `serve()`, `serveStatic`, `ServerType`
  — is unchanged, so no source edits were needed.
- `vite` ^8.0.0 -> ^8.1.5 (root, clients/web) and ^8.0.16 -> ^8.1.5
  (clients/tui) — the security bumps from #1841.

`npm install` at the root (postinstall cascades into every client)
regenerated the three touched lockfiles. `npm run ci` passes.


Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…its work dir (#1895)

* fix(scripts): await the pack:verify web child's exit before removing its work dir

`pack-and-verify.mjs` had the same kill-then-remove shape that made
`smoke:tui` flake with ENOTEMPTY in #1801: `verifyWeb()`'s
`finally { stop() }` only *delivered* SIGTERM, then the main flow
`rmSync`'d the work dir immediately, and `fail()` did the same back to
back.

Applies the #1814 treatment, factored into a shared
`scripts/lib/child-cleanup.mjs` so the two scripts can't drift:

- `stopChild()` — SIGTERM, await the child's `exit`/`close`, escalate to
  SIGKILL after a grace period, and ultimately proceed with a warning so
  a wedged child can never hang the script. `verifyWeb()`'s `stop()` now
  goes through it and is awaited, so the child is gone before the caller
  removes `work`.
- `removeSafe()` — `rmSync` that warns instead of throwing. Used on both
  success-path removals (a leftover temp dir must never fail a run that
  passed) and in `fail()`, where an ENOTEMPTY could otherwise bury the
  real diagnostic under an rmSync stack and skip the "tarball retained
  at ..." hint. `fail()` stays synchronous — it is called from sync
  contexts and ends in `process.exit()` — so it best-effort signals the
  child; `removeSafe()` is what makes that safe.

`smoke-tui.mjs` adopts `removeSafe()` for its `cleanup()`. Its `done()`
keeps its bespoke wait (documented inline): that one must keep waiting
for `close` *after* `exit` on a shorter re-armed deadline so the quoted
output is complete, which `stopChild()`'s first-of-exit/close contract
deliberately does not do.

Adds `scripts/lib/child-cleanup.test.mjs` (10 cases, `node --test` via
`npm run test:scripts`) covering the SIGTERM/SIGKILL escalation, the
`close`-only spawn-failure path, the never-hang give-up, late events
after giving up, and that `removeSafe` warns rather than throws.

`npm run ci` passes, and `npm run pack:verify` (not in the gate — needs
network) was run end to end and passes with no cleanup warnings.

Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(scripts): normalize stopChild's graceMs and correct removeSafe's JSDoc

Copilot review round 1 on #1895.

- `stopChild()` trusted `graceMs` as given, but call sites build it as
  `Number(process.env.X ?? 5000)` — a typo yields `NaN`, which
  `setTimeout` treats as `0`. That would fire SIGTERM and SIGKILL in the
  same tick and print "did not exit within NaNms", collapsing the whole
  escalation silently. New exported `normalizeGraceMs()` falls back to
  `DEFAULT_EXIT_GRACE_MS` for anything non-finite or non-positive, with
  two tests: the table of bad values, and an end-to-end one asserting a
  `NaN` grace period still leaves only SIGTERM sent a turn later.
- `removeSafe()`'s `@returns` said "true if the path was removed", which
  is inaccurate for a missing path (`force: true` makes it a no-op
  success). Reworded to say what it actually reports.

`npm run ci` passes.

Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ocument the triage rubric (#1892)

* docs: add Incoming board status, adopt Priority field, document triage rubric (#1891)

Board #28 had no state for "filed but not yet triaged" — Todo means
maintainer-approved, so parking unreviewed issues there falsely asserted
sign-off. Adds an Incoming status ahead of Todo.

The board's Priority single-select existed but was unused (0 of 264 cards)
and undocumented, with P0/P1/P2 options matching no vocabulary in the repo.
Re-optioned to Urgent/High/Medium/Low and documented alongside Status.

Completes the priority rubric: fills in the score bands (12+/9-11/6-8/<=5),
expands both axes into scored tables, and fixes the truncated signal-indicator
line. Severity alone tops out at High by design.

Also drops the "V2 Go Live" status reference — that option is not in the
field's option list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

* docs: correct the Done option id and harden the deleted-column guidance (#1891)

The Done Status option was deleted from board #28, blanking the Status of
all 247 cards that held it. Restored from a pre-change item-list snapshot;
verified all 247 are Done again and no other card was disturbed.

A recreated option never regains its old id, so Done is now 259d6aab rather
than 248a3910 — updated in the option table and the merge step.

The hazard callout recommended the web UI as the safe path, which is what
made this look safe. It is not: deleting an option blanks its cards in the
UI exactly as it does via the API, with no undo. Corrects that, adds a
snapshot-first rule, and documents the recovery recipe used here — including
the grouping check that proves the orphaned set is exactly the deleted
option's cards before re-applying.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

* docs: add Incoming to the v1 board and document its recipes (#1891)

Board #11 had the same gap as #28 — no state for "filed but not yet
triaged" — so a v1 security-fix issue had nowhere to sit that didn't
assert maintainer approval. Adds Incoming there too.

AGENTS.md carried no v1 board recipes at all, only a link, so anyone
boarding a v1 issue had to discover the project and field ids by hand
and was liable to reach for #28's (which are rejected, but only after
the attempt). Adds a V1 board section with its ids, states that Incoming
is the default status for new items on both boards, and notes that
Priority is v2-only — #11 has no such field.

The Incoming option was added with all four existing option ids echoed
back, and a before/after item-list diff confirms all 74 cards kept their
Status. Three cards on #11 carry no Status; that predates this change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

* docs: distinguish the board Priority from the org-level issue field (#1891)

An issue page shows two fields named "Priority" and nothing syncs them.
Ours is the project field on board #28. The other is a GitHub issue field
(IFSS_kgDOAdAWeg) defined at the modelcontextprotocol org and shared by
every repo in it, alongside Effort/Start date/Target date. Same name, same
four option names, unrelated storage — #1891 sat at Urgent in Fields and
High on the board simultaneously.

Adds a table naming both, states that no pass-through exists in either
direction, and says to leave the org-level one alone: it is not repo-scoped,
so deleting it would strip Priority from every other org repo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

* docs: mark the org Priority field untrusted and cap it at a +1 signal (#1891)

Both boards are private (public: false, verified), so maintainer-assigned
Status and Priority are invisible to reporters — board priority is a
working queue, not a published commitment.

The org-level Fields -> Priority is the opposite: public on the issue page
and not part of triage. A value there is a reporter's preference, not an
assessment, so it is untrusted input. It now feeds the rubric as a sixth
signal bonus — flat +1 for Urgent or High, identical for both — and never
maps to a band. Urgent needs 12, so nothing a reporter types reaches it
alone; the issue must already sit at 11 on maintainer-assessed axes.

Supersedes the earlier "it is noise" framing: the signal is worth keeping,
it just cannot be allowed to decide the outcome. Also corrects the band
arithmetic (2-15 -> 2-16) now that a sixth bonus exists, and notes that
scores recorded on 2026-08-01 cite the old /15 denominator.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

* docs: drop the /15 denominator caveat now the recorded scores are restated (#1891)

The 51 triage comments were rewritten from /15 to /16, so the note saying
they cite the old denominator no longer describes anything. No total moved:
the reporter-set signal only scores for Urgent or High, and the one scored
issue carrying an org-field value (#1826) had Low, which earns nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

* docs: describe the board-option hazard per field, not Status-only (#1891)

Copilot review on #1892 flagged three places that still read as
Status-only now that the section covers Priority too:

- the version-label bullet said a v2 issue needs "a board card with a
  Status"; it needs a Status and a Priority (v1 needs only a Status,
  since board #11 has no Priority field)
- the updateProjectV2Field hazard said a bad edit "orphans the Status
  of every card"; it orphans whichever field was edited
- the recovery recipe is genuinely Status-specific (reads .status,
  writes the Status field id), so say so and give the two
  substitutions for a Priority deletion

Verified `gh project item-list --format json` exposes both .status and
.priority before documenting the .priority swap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: replace the markdown bug template with issue forms (#1844)

Issues are the only intake channel now that external pull requests are off
(#1820), so the chooser is where the contribution policy has to be stated —
GitHub gives a would-be contributor no explanation when the "New pull request"
button isn't there.

The one existing template was a legacy **markdown** template, whose fields are
prose headings a reporter can delete or ignore. Most reports still arrive
missing the client and the transport, which are the two facts that decide where
to look. Issue forms enforce them: this replaces `bug_report.md` with
`1-bug_report.yml`, where client (web/cli/tui/core), version line (v1/v2),
Inspector version, Node version, transport, MCP server, repro, expected, and
actual are all `validations.required`. It auto-labels `bug`, and its intro says
maintainers implement the fix, so a prompt plus screenshots beats a diff —
with a field for exactly that.

Adds `2-feature_request.yml` (auto-labels `enhancement`), which puts the
problem statement first and marks the solution optional, since the problem is
what survives when a specific solution turns out not to fit.

Neither form applies a `v1`/`v2` label: GitHub can't map a form answer to a
label, so the version line is a required dropdown and a maintainer still labels
at triage. v1 takes security fixes only, so routing that answer early is the
point.

`config.yml` disables blank issues and carries the contact links. **There is
deliberately no security *template*** — a template still opens a public issue,
which is precisely what a vulnerability report must not do. The redirect is a
contact link straight to the private advisory form (`/security/advisories/new`,
verified enabled on this repo) plus one to `SECURITY.md`, which covers the v1
line too. The remaining links deflect the recurring misfiled classes: the
specification repo and the TypeScript SDK for reports that aren't about this
tool, the docs site, the contribution policy, and `#inspector-dev`.

Also fixes three links the #1884 rename left dangling — `SECURITY.md` and the
PR template still pointed at `CONTRIBUTORS.md`, which no longer exists on this
branch. A policy nobody can open is the same as no policy.

Validated against GitHub's issue-forms schema (unique ids, allowed `type`s,
`markdown` blocks carrying no `id`/`validations`, `checkboxes` marking
`required` per option rather than under `validations`).

Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: address Copilot review on the issue forms (#1844)

- PR template said `v2` targets `main`; the v2 base branch is `v2/main`.
  Correct on its face, and the one line in that template a reader acts on.
- Scoped the Node floor on the bug form to v2, since the form serves both
  lines and v1's floor is older — reading `>= 22.19.0` as universal would
  make a legitimate v1 report look out of support.
- Both the CONTRIBUTING.md paragraph and the AGENTS.md section now say the
  chooser is served from the **default branch**, so neither claims a form
  added on `v2/main` is live before the milestone merge.

* docs: re-wrap the CONTRIBUTING chooser paragraph (#1844)

Round-2 Copilot nit: the previous wrap left a dangling "The" at end of
line, splitting a phrase mid-sentence in the raw markdown.

Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: clarify the protocol-era prompt on the bug form (#1844)

Round-3 Copilot nit. "the protocol era (legacy / modern) if you selected
one" reads as referring to a field on this form, which has none — the era is
selected on the *connection*, in the Inspector's server settings. Reworded to
"the protocol era you connected with (auto / legacy / modern)", which also
picks up `auto`, the default the previous list omitted.

Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: stop describing the feature form as sharing the bug form's fields (#1844)

Round-4 Copilot nits, and it was right three times over — I had described
"client, version line, transport" as the required set for *both* forms in
CONTRIBUTING.md, AGENTS.md, and the config.yml header. The feature form has
neither a version-line dropdown nor a transport field, and cannot have the
first: v1 takes security fixes only, so a feature request is v2 by
construction.

Each of the three now distinguishes the two forms rather than generalizing
from the bug form.

Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: declare v2 statically on the feature form (#1844)

Round-5 Copilot nit caught a real contradiction: the AGENTS.md paragraph said
no form can apply a version label, then asserted the feature form "is always
labeled v2". Both halves can't be true.

Resolved in the direction that makes the claim true rather than softer. A
form's `labels:` is static, which is only an obstacle when the value depends
on a reporter's answer — and for the feature form it doesn't: v1 takes security
fixes only, so a feature request is v2 by construction. So it now declares
`["enhancement", "v2"]` outright, which also satisfies AGENTS.md's
label-at-create-time rule without a triage step.

The bug form still can't, and shouldn't: its version line is genuinely the
reporter's to answer.

Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…SA-p63j-vcc4-9vmv (#1839) (#1899)

GHSA-p63j-vcc4-9vmv (**critical**) — `@vitest/browser` Browser Mode provider
commands accept a file path from the browser and act on it without checking the
`allowWrite` permission gate. Patched in 4.1.10; `clients/web` was pinned at
4.1.0.

Dev-scope only — `@vitest/*` is absent from the published `files` allowlist, and
Browser Mode here runs this project's own Storybook play functions, never a
third-party page. That is why #1837 deferred it rather than attempting a
toolchain upgrade immediately before an irreversible publish.

## The knot, and what actually unties it

`^4.1.0` already admits 4.1.10, so the declared ranges were never the problem.
The family is welded by *exact* peer pins — `vitest` pins
`@vitest/browser-playwright`, which pins `@vitest/browser` — so nothing moves
alone.

The issue predicted that an explicit coordinated `npm i -D` would fix it. It
does not. With `clients/web/package-lock.json` in place, npm ERESOLVEs even when
every member of the family plus the Storybook packages are named in one command
and `node_modules` is deleted first:

    Found: @storybook/addon-a11y@10.2.19
    Could not resolve dependency: dev @storybook/addon-a11y@"^10.5.5"
    Conflicting peer dependency: storybook@10.5.5

npm resolves against the existing lock tree and will not move the whole
constellation at once. **Regenerating `clients/web/package-lock.json` is the
only thing that unties it** — a clean resolve places all four at 4.1.10 with no
peer complaints at all.

## Versions

    vitest                       4.1.0    -> 4.1.10
    @vitest/browser              4.1.0    -> 4.1.10   (transitive)
    @vitest/browser-playwright   4.1.0    -> 4.1.10
    @vitest/coverage-v8          4.1.0    -> 4.1.10
    storybook + @storybook/*     10.2.19  -> 10.5.5
    eslint-plugin-storybook      10.2.19  -> 10.5.5
    @chromatic-com/storybook     5.0.1    -> 5.2.1

`clients/{cli,tui,launcher}` were already resolving 4.1.9 (no browser pins, so
they floated freely) and were never vulnerable. Their declared `^4.1.0` still
*permitted* 4.1.0 on a fresh resolve, so the floor is raised to `^4.1.10` in all
four manifests — same reasoning as the vite floor in #1841. Pinning only the
lockfile would have left a regression path open.

## The vite 8 peer override

Resolved, as a consequence rather than a workaround.
`@joshwooding/vite-plugin-react-docgen-typescript@0.6.4` declared
`peer vite@"^3 || ^4 || ^5 || ^6 || ^7"` against our vite 8. It is not a direct
dependency — it arrives through `@storybook/react-vite`, which at 10.5.5 depends
on `^0.7.0`, and 0.7.0 adds `^8.0.0` to that range. So taking Storybook 10.5.5
for the peer set drops the override too; nothing is forced.

## Two holds the regeneration forced, each with a tracked issue

Regenerating the lock floats every other in-range dependency to current. Most of
that is welcome — it is what takes `clients/web` from 19 advisories to 1 — but
two floats broke the gate and are unrelated to this security fix:

- **zod 4.3.6 -> 4.4.3** makes `clients/web`'s `tsc -b` die with
  `FATAL ERROR: Ineffective mark-compacts near heap limit` at the ~4GB default.
  Reverting zod alone makes it exit 0, so the attribution is unambiguous.
  Held at `~4.3.6`. See #1896.
- **eslint-plugin-react-hooks 7.0.1 -> 7.1.1** enables `set-state-in-effect`,
  which fails `lint` on 8 pre-existing violations across 7 components. Fixing
  them is a real refactor with interaction-behavior consequences, not a
  suppression. Held at `~7.0.1`. See #1897.

Both `~` pins are deliberate and temporary, and both issues say so — they are
constraints to remove, not preferences.

Storybook 10.5.5 also reports that `setProjectAnnotations` is applied
automatically since 10.3, making `.storybook/vitest.setup.ts` redundant. Left in
place: `./preview` carries the Mantine decorator and the a11y annotations drive
the play-function assertions, and if automatic provisioning missed either, the
462 stories would render unthemed with a11y checks inert and very likely still
pass. That silent-failure risk does not belong in a security bump. See #1898.

## Audit delta

`clients/web`, where the advisory lived:

    before   19 (4 critical, 6 high, 6 moderate, 3 low)
    after     1 (1 low — esbuild-in-tsup, Windows dev server only)

All four critical entries — `vitest`, `@vitest/browser`,
`@vitest/browser-playwright`, `@vitest/coverage-v8` — are gone. The other 14 are
transitive dev-tree advisories (hono, ws, js-yaml, flatted, brace-expansion, ...)
cleared as a side effect of the clean resolve. Elsewhere: cli 6 -> 5,
launcher 3 -> 2, tui 4 -> 4, root unchanged at 9 (its lock is untouched). Those
remainders are pre-existing and unrelated.

## Verification

`npm run ci` green end to end on the upgraded stack — the point of caring here,
since this *is* the test infrastructure:

- coverage gate (>=90 per file, all four dimensions) passes on all four clients;
  no file regressed, so no gate was lowered and no `v8 ignore` added
- 4802 web tests, 304 cli, 282 tui, 5 launcher
- 462 Storybook play functions across 109 files, on vitest 4.1.10 Browser Mode
- verify:build-gate, and all five smokes including `smoke:tui` (real TTY here)

Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…visory (#1900)

* chore(deps): migrate to eslint 10 to clear the brace-expansion DoS advisory (#1838)

eslint 9 pulled `minimatch@3` → `brace-expansion@1.1.16`, which carries
GHSA-mh99-v99m-4gvg (high, DoS via unbounded expansion length). It appeared in
all five lockfiles — the root plus each of the four clients, none of which is an
npm workspace, so all five had to move together.

Deferred from the pre-2.0.0 sweep because it is dev-scope, not attacker-
controlled, and a major bump across five packages was the wrong thing to attempt
immediately before an irreversible publish. This is that work.

## What moved

- `eslint` `^9.39.4/5` → `^10.8.0` and `@eslint/js` → `^10.0.1`, in all five
  manifests.
- `eslint-plugin-react-hooks` `^7.0.1` → `^7.1.1` in web and tui. Required, not
  cosmetic: 7.0.1's peer range stops at `^9.0.0`, so installing it alongside
  eslint 10 fails `ERESOLVE`.
- Everything else already declared eslint 10 support and stayed put:
  `typescript-eslint@^8.56.1` (`^8.57 || ^9 || ^10`),
  `eslint-plugin-react-refresh@^0.5.2` (`^9 || ^10`),
  `eslint-plugin-storybook@^10.2.19` (`>=8`).

## Config migration

None was needed. All five configs were already flat (`eslint.config.js` with
`defineConfig`/`globalIgnores` from `eslint/config`), which is the format v10
keeps and the only one it still supports. The root config's `core/` and shared-
surface blocks — including the `_`-prefix `argsIgnorePattern` /
`varsIgnorePattern` / `caughtErrorsIgnorePattern` — are unchanged and still gate
`lint:core` and `lint:shared`.

No rule was disabled, downgraded, or scoped away to make the new eslint pass.
v10 surfaced 21 real violations across three rules; all 21 are fixed in the code.

### `preserve-caught-error` (new in `eslint:recommended`) — 15 sites

A `throw new Error(msg)` inside a `catch` silently drops the original error.
Each now passes `{ cause }`, so the underlying failure survives into the chain.
Messages are unchanged, so nothing that asserts on them moves. 12 in `core/`,
2 in `clients/cli`, 1 in `test-servers/src`.

### `no-useless-assignment` (new in `eslint:recommended`) — 2 sites

`clients/cli/src/cli.ts`'s `optionArgs` and `test-servers/src/test-server-oauth`
's `valid` were initialized and then unconditionally overwritten on every path.
Both are now declared without the dead initializer; TypeScript's definite-
assignment analysis covers the remaining paths.

### `react-hooks/set-state-in-effect` — 8 sites (web)

These were false negatives the plugin fixed in 7.1.0, not a rule we newly opted
into: `set-state-in-effect` has been `error` in `flat/recommended` since 7.0.1,
and the web config has always extended it. Every one was the same anti-pattern —
resetting or re-syncing local state from a prop inside a `useEffect`, which
paints the stale value for one frame and then renders again to correct it.

All eight now use a new `useValueChange(value, onChange)` hook
(`clients/web/src/hooks/useValueChange.ts`), which is React's documented
"adjusting state during render": compare against the previous render's value
with `Object.is` and call back *during* render, so React discards the
in-progress output before it reaches the DOM.

- `MrtrConversation`, `ProtocolEntry`, `TaskCard` — mirror `isListExpanded`.
- `NetworkEntry` — same mirror, plus the "Reveal in Network" force-open. The
  reveal's rAF scroll stays an effect (real external-system work); only the
  `setIsExpanded(true)` moved. `isExpanded` is now seeded from
  `isListExpanded || revealed`, since a render-time sync fires on change and so
  cannot cover an entry that mounts already revealed.
- `PromptArgumentsForm`, `ResourceTemplatePanel` — reset on template/prompt
  switch.
- `ServerConfigModal` — reset on open. Keyed on `opened ? initial : undefined`,
  which collapses "opened flipped" and "initial changed while open" into one
  value; `initial` is already memoized, so it cannot thrash.

`useValueChange` has its own test file, and `ServerConfigModal` gains an
open→edit→close→reopen test (the close leg was previously untested, which is
what left the new guard uncovered).

## Advisory delta

`npm audit`, per package, before → after:

| Package             | before                     | after                      |
| ------------------- | -------------------------- | -------------------------- |
| root                | 9 (1 low, 6 mod, 2 high)   | 8 (1 low, 6 mod, 1 high)   |
| `clients/web`       | 19 (3 low, 6 mod, 6 high, 4 crit) | 17 (3 low, 6 mod, 4 high, 4 crit) |
| `clients/cli`       | 6 (2 low, 4 high)          | 4 (2 low, 2 high)          |
| `clients/tui`       | 4 (1 low, 3 high)          | 2 (1 low, 1 high)          |
| `clients/launcher`  | 3 (3 high)                 | 1 (1 high)                 |

`brace-expansion` is gone from all five — `eslint@10` resolves `minimatch@10` →
`brace-expansion@5.0.9`. Dropping `@eslint/eslintrc` (removed in v10) also took
`js-yaml` with it, clearing GHSA-52cp-r559-cp3m (high) from all four clients as
a bonus. The remainder are unrelated and tracked separately.

`npm run ci` passes end to end — validate, the per-file ≥90 coverage gate, the
build gate, all five smokes (including `smoke:tui`, run locally on a real TTY),
and 462 Storybook tests.

Also documents the effect rule in AGENTS.md and mirrors it into
`.github/copilot-instructions.md`, since it is now something a reviewer cites.

Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: state useValueChange's referential-stability requirement (#1838)

Copilot review on #1900: the hook compares with `Object.is`, so an object or
array literal rebuilt in the component body compares unequal every render.
Because the callback is what updates state, that is not merely a wasted render —
it is an infinite loop.

Every current caller passes a primitive (`boolean`, `string`) or an already-
memoized value, so nothing changes behaviorally. But the hazard was implicit,
and the next caller is the one it would bite.

Documented on the hook itself, and one clause added to the AGENTS.md rule with
its mirror in `.github/copilot-instructions.md`. No runtime guard: the
requirement is the same one a `useEffect` dependency array carries, and here the
failure is loud and immediate rather than silent.

`npm run ci` passes.

Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(cli): stop asserting the --tool-args-json catch binding is an Error (#1838)

Copilot review round 2 on #1900: the `--tool-args-json` parse handler formatted
its message with `(e as Error).message`. A `catch` binding is `unknown`, so that
cast is an assertion rather than a check — a thrown non-Error would render
`--tool-args-json is not valid JSON: undefined`, which points the user at
nothing.

`JSON.parse` does only throw `SyntaxError` today, so this is not a live bug. But
the cast is the kind AGENTS.md asks us not to leave unjustified, the same
handler was already being touched here to add `{ cause: e }`, and every other
site in this PR uses the guarded form. Now it does too.

`npm run ci` passes.

Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: require useValueChange's onChange to be render-pure (#1838)

Copilot review round 3 on #1900: `onChange` is called during render, so it is
bound by render-phase purity — and the web client mounts under `StrictMode`,
which deliberately double-renders in development. A callback doing anything
external would run an unpredictable number of times, and concurrent React can
abandon an in-progress render entirely.

Every current caller already passes a callback that only calls `setState`, so
nothing changes. But "runs during render" is the whole point of the hook and the
constraint that follows from it was left implicit.

Stated on the hook, with the same clause in the AGENTS.md rule and its
`.github/copilot-instructions.md` mirror. All three now point at `NetworkEntry`
as the worked example of the split: the reveal's force-open is a state update
and lives in `useValueChange`, while its `requestAnimationFrame` scroll stays a
`useEffect`.

`npm run ci` passes.

Claude-Session: https://claude.ai/code/session_01YAt8rqxysNbhYWLhoRm3fU

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cliffhall

Copy link
Copy Markdown
Member Author

Superseded by #1903.

This PR was v2/mainmain directly, which came up CONFLICTING in 8 files. Resolving them here would have meant GitHub's web conflict editor, which commits to the head branch — merging main into v2/main, exactly the back-merge #1868 was closed to avoid.

#1903 instead merges v2/main into a branch cut from main (v2/chore/milestone-merge-v2.1.0), so v2/main's ancestry is untouched while the merge commit still carries its tip (834fd2d7) as the second parent. All eight conflicts resolved to v2/main's side, npm run ci green on the merged tree.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2 Issues and PRs for v2

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Execute the first v2/main → main milestone merge and cut the release

2 participants